Elasticsearch:介绍 kNN query,这是进行 kNN 搜索的专家方法

这篇具有很好参考价值的文章主要介绍了Elasticsearch:介绍 kNN query,这是进行 kNN 搜索的专家方法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

作者:来自 Elastic Mayya Sharipova, Benjamin Trent

Elasticsearch:介绍 kNN query,这是进行 kNN 搜索的专家方法,Elasticsearch,AI,Elastic,elasticsearch,大数据,搜索引擎,人工智能,全文检索,数据库

当前状况:kNN 搜索作为顶层部分

Elasticsearch 中的 kNN 搜索被组织为搜索请求的顶层(top level)部分。 我们这样设计是为了:

  • 无论分片数量多少,它总是可以返回全局 k 个最近邻居
  • 这些全局 k 个结果与其他查询的结果相结合以形成混合搜索
  • 全局 k 结果被传递到聚合以形成统计(facets)。

这是 kNN 搜索在内部执行的简化图(省略了一些阶段):

Elasticsearch:介绍 kNN query,这是进行 kNN 搜索的专家方法,Elasticsearch,AI,Elastic,elasticsearch,大数据,搜索引擎,人工智能,全文检索,数据库

图 1:顶层 kNN 搜索的步骤是:

  1. 用户提交搜索请求
  2. 协调器节点在 DFS 阶段向数据节点发送请求的 kNN 搜索部分
  3. 每个数据节点运行 kNN 搜索并将本地 top-k 结果发送回协调器
  4. 协调器合并所有本地结果以形成全局前 k 个最近邻居。
  5. 协调器将全局 k 个最近邻居发送回数据节点,并提供任何其他查询
  6. 每个数据节点运行额外的查询并将本地 size 结果发送回协调器
  7. 协调器合并所有本地结果并向用户发送响应

我们首先在 DFS 阶段运行 kNN 搜索以获得全局前 k 个结果。 然后,这些全局 k 结果被传递到搜索请求的其他部分,例如其他查询或聚合。 即使执行看起来很复杂,但从用户的角度来看,运行 kNN 搜索的模型很简单,因为用户始终可以确保 kNN 搜索返回全局 k 结果。

它的请求格式如下:

GET collection-with-embeddings/_search
{
  "knn": {
    "field": "text_embedding.predicted_value",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": "sentence-transformers__msmarco-distilbert-base-tas-b",
        "model_text": "How is the weather in Jamaica?"
      }
    },
    "k": 10,
    "num_candidates": 100
  },
  "_source": [
    "id",
    "text"
  ]
}

引入 kNN 查询

随着时间的推移,我们意识到还需要将 kNN 搜索表示为查询。 查询是 Elasticsearch 中搜索请求的核心组件,将 kNN 搜索表示为查询可以灵活地将其与其他查询结合起来,以解决更复杂的请求。

kNN 查询与顶层 kNN 搜索不同,没有 k 参数。 与其他查询一样,返回的结果(最近邻居)的数量由 size 参数定义。 与 kNN 搜索类似,num_candidates 参数定义在执行 kNN 搜索时在每个分片上考虑多少个候选者。

GET products/_search
{
 "size" : 3,
 "query": {
   "knn": {
     "field": "embedding",
     "query_vector": [2,2,2,0],
     "num_candidates": 10
   }
 }
}

kNN 查询的执行方式与顶层 kNN 搜索不同。 下面是一个简化图,描述了 kNN 查询如何在内部执行(省略了一些阶段):

Elasticsearch:介绍 kNN query,这是进行 kNN 搜索的专家方法,Elasticsearch,AI,Elastic,elasticsearch,大数据,搜索引擎,人工智能,全文检索,数据库

图 2:基于查询的 kNN 搜索步骤如下:

  • 用户提交搜索请求
  • 协调器向数据节点发送一个 kNN 搜索查询,并提供附加查询
  • 每个数据节点运行查询并将本地大小结果发送回协调器节点
  • 协调器节点合并所有本地结果并向用户发送响应

我们在一个分片上运行 kNN 搜索以获得 num_candidates 结果; 这些结果将传递给分片上的其他查询和聚合,以从分片获取大小结果。 由于我们不首先收集全局 k 个最近邻居,因此在此模型中,收集的且对其他查询和聚合可见的最近邻居的数量取决于分片的数量。

kNN 查询 API 示例

让我们看一下 API 示例,这些示例演示了顶层 kNN 搜索和 kNN 查询之间的差异。

我们创建产品索引并索引一些文档:

PUT products
{
 "mappings": {
   "dynamic": "strict",
   "properties": {
     "department": {
       "type": "keyword"
     },
     "brand": {
       "type": "keyword"
     },
     "description": {
       "type": "text"
     },
     "embedding": {
       "type": "dense_vector",
       "index": true,
       "similarity": "l2_norm"
     },
     "price": {
       "type": "float"
     }
   }
 }
}
POST products/_bulk?refresh=true
{"index":{"_id":1}}
{"department":"women","brand": "Levi's", "description":"high-rise red jeans","embedding":[1,1,1,1],"price":100}
{"index":{"_id":2}}
{"department":"women","brand": "Calvin Klein","description":"high-rise beautiful jeans","embedding":[1,1,1,1],"price":250}
{"index":{"_id":3}}
{"department":"women","brand": "Gap","description":"every day jeans","embedding":[1,1,1,1],"price":50}
{"index":{"_id":4}}
{"department":"women","brand": "Levi's","description":"jeans","embedding":[2,2,2,0],"price":75}
{"index":{"_id":5}}
{"department":"women","brand": "Levi's","description":"luxury jeans","embedding":[2,2,2,0],"price":150}
{"index":{"_id":6}}
{"department":"men","brand": "Levi's", "description":"jeans","embedding":[2,2,2,0],"price":50}
{"index":{"_id":7}}
{"department":"women","brand": "Levi's", "description":"jeans 2023","embedding":[2,2,2,0],"price":150}

kNN 查询类似于顶层 kNN 搜索,具有 num_candidates 和充当预过滤器的内部 filter 参数。

GET products/_search?filter_path=**.hits
{
 "size" : 3,
 "query": {
   "knn": {
     "field": "embedding",
     "query_vector": [2,2,2,0],
     "num_candidates": 10,
     "filter" : {
       "term" : {
         "department" : "women"
       }
     }
   }
 }
} 
{
  "hits": {
    "hits": [
      {
        "_index": "products",
        "_id": "4",
        "_score": 1,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 75
        }
      },
      {
        "_index": "products",
        "_id": "5",
        "_score": 1,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "luxury jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 150
        }
      },
      {
        "_index": "products",
        "_id": "7",
        "_score": 1,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "jeans 2023",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 150
        }
      }
    ]
  }
}

kNN 查询比 kNN collapsing 和聚合搜索可以获得更多样化的结果。 对于下面的 kNN 查询,我们在每个分片上执行 kNN 搜索以获得 10 个最近邻居,然后将其传递到 collapsing 以获取 3 个顶部结果。 因此,我们将在响应中得到 3 个不同的点击。

GET products/_search?filter_path=**.hits
{
 "size" : 3,
 "query": {
   "knn": {
     "field": "embedding",
     "query_vector": [2,2,2,0],
     "num_candidates": 10,
     "filter" : {
       "term" : {
         "department" : "women"
       }
     }
   }
 },
 "collapse": {
   "field": "brand"        
 }
}
{
  "hits": {
    "hits": [
      {
        "_index": "products",
        "_id": "4",
        "_score": 1,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 75
        },
        "fields": {
          "brand": [
            "Levi's"
          ]
        }
      },
      {
        "_index": "products",
        "_id": "2",
        "_score": 0.2,
        "_source": {
          "department": "women",
          "brand": "Calvin Klein",
          "description": "high-rise beautiful jeans",
          "embedding": [
            1,
            1,
            1,
            1
          ],
          "price": 250
        },
        "fields": {
          "brand": [
            "Calvin Klein"
          ]
        }
      },
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.2,
        "_source": {
          "department": "women",
          "brand": "Gap",
          "description": "every day jeans",
          "embedding": [
            1,
            1,
            1,
            1
          ],
          "price": 50
        },
        "fields": {
          "brand": [
            "Gap"
          ]
        }
      }
    ]
  }
}

顶层 kNN 搜索首先在 DFS 阶段获取全局前 3 个结果,然后在查询阶段将它们传递到 collapse。 我们在响应中只会得到 1 个命中,因为全球 3 个最近的邻居恰好都来自同一品牌。

与聚合类似,kNN query 允许我们获得 3 个不同的存储桶,而 kNN search 仅允许 1 个。

GET products/_search?filter_path=aggregations
{
"size": 0,
"query": {
   "knn": {
     "field": "embedding",
     "query_vector": [2,2,2,0],
     "num_candidates": 10,
     "filter" : {
       "term" : {
         "department" : "women"
       }
     }
   }
 },
 "aggs": {
   "brands": {
     "terms": {
       "field": "brand"
     }
   }
 }
}
{
  "aggregations": {
    "brands": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "Levi's",
          "doc_count": 4
        },
        {
          "key": "Calvin Klein",
          "doc_count": 1
        },
        {
          "key": "Gap",
          "doc_count": 1
        }
      ]
    }
  }
}

而顶层的 search 是这样的:

GET products/_search?filter_path=aggregations
{
  "size": 0,
  "knn": {
    "field": "embedding",
    "query_vector": [
      2,
      2,
      2,
      0
    ],
    "k": 3,
    "num_candidates": 10,
    "filter": {
      "term": {
        "department": "women"
      }
    }
  },
  "aggs": {
    "brands": {
      "terms": {
        "field": "brand"
      }
    }
  }
}
{
  "aggregations": {
    "brands": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "Levi's",
          "doc_count": 3
        }
      ]
    }
  }
}

现在,让我们看一下其他示例,展示 kNN 查询的灵活性。 具体来说,它如何能够灵活地与其他查询结合起来。

kNN 可以是 boolean 查询的一部分(需要注意的是,所有外部查询过滤器都用作 kNN 搜索的后过滤器)。 我们可以使用 kNN 查询的 _name 参数来通过额外信息来增强结果,这些信息告诉 kNN 查询是否匹配及其分数贡献。

GET products/_search?include_named_queries_score
{
 "size": 3,
 "query": {
   "bool": {
     "should": [
       {
         "knn": {
           "field": "embedding",
           "query_vector": [2,2,2,0],
           "num_candidates": 10,
           "_name": "knn_query"
         }
       },
       {
         "match": {
           "description": {
             "query": "luxury",
             "_name": "bm25query"
           }
         }
       }
     ]
   }
 }
}
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 7,
      "relation": "eq"
    },
    "max_score": 2.8042283,
    "hits": [
      {
        "_index": "products",
        "_id": "5",
        "_score": 2.8042283,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "luxury jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 150
        },
        "matched_queries": {
          "knn_query": 1,
          "bm25query": 1.8042282
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 1,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 75
        },
        "matched_queries": {
          "knn_query": 1
        }
      },
      {
        "_index": "products",
        "_id": "6",
        "_score": 1,
        "_source": {
          "department": "men",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 50
        },
        "matched_queries": {
          "knn_query": 1
        }
      }
    ]
  }
}

kNN 也可以是复杂查询的一部分,例如 pinned 查询。 当我们想要显示最接近的结果,但又想要提升选定数量的其他结果时,这非常有用。

GET products/_search
{
 "size": 3,
 "query": {
   "pinned": {
     "ids": [ "1", "2" ],
     "organic": {
       "knn": {
           "field": "embedding",
           "query_vector": [2,2,2,0],
           "num_candidates": 10,
           "_name": "knn_query"
         }
     }
   }
 }
}
{
  "took": 9,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 7,
      "relation": "eq"
    },
    "max_score": 1.7014124e+38,
    "hits": [
      {
        "_index": "products",
        "_id": "1",
        "_score": 1.7014124e+38,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "high-rise red jeans",
          "embedding": [
            1,
            1,
            1,
            1
          ],
          "price": 100
        },
        "matched_queries": [
          "knn_query"
        ]
      },
      {
        "_index": "products",
        "_id": "2",
        "_score": 1.7014122e+38,
        "_source": {
          "department": "women",
          "brand": "Calvin Klein",
          "description": "high-rise beautiful jeans",
          "embedding": [
            1,
            1,
            1,
            1
          ],
          "price": 250
        },
        "matched_queries": [
          "knn_query"
        ]
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 1,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 75
        },
        "matched_queries": [
          "knn_query"
        ]
      }
    ]
  }
}

我们甚至可以将 kNN 查询作为 function_score 查询的一部分。 当我们需要为 kNN 查询返回的结果定义自定义分数时,这非常有用:​

GET products/_search
{
 "size": 3,
 "query": {
   "function_score": {
     "query": {
       "knn": {
           "field": "embedding",
           "query_vector": [2,2,2,0],
           "num_candidates": 10,
           "_name": "knn_query"
         }
     },
     "functions": [
       {
         "filter": { "match": { "department": "men" } },
         "weight": 100
       },
       {
         "filter": { "match": { "department": "women" } },
         "weight": 50
       }
     ]
   }
 }
}
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 7,
      "relation": "eq"
    },
    "max_score": 100,
    "hits": [
      {
        "_index": "products",
        "_id": "6",
        "_score": 100,
        "_source": {
          "department": "men",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 50
        },
        "matched_queries": [
          "knn_query"
        ]
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 50,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 75
        },
        "matched_queries": [
          "knn_query"
        ]
      },
      {
        "_index": "products",
        "_id": "5",
        "_score": 50,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "luxury jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 150
        },
        "matched_queries": [
          "knn_query"
        ]
      }
    ]
  }
}

当我们想要组合 kNN 搜索和其他查询的结果时,kNN 查询作为 dis_max 查询的一部分非常有用,以便文档的分数来自排名最高的子句,并为任何其他子句提供打破平局的增量。

GET products/_search
{
 "size": 5,
 "query": {
   "dis_max": {
     "queries": [
       {
         "knn": {
           "field": "embedding",
           "query_vector": [2,2, 2,0],
           "num_candidates": 3,
           "_name": "knn_query"
         }
       },
       {
         "match": {
           "description": "high-rise jeans"
         }
       }
     ],
     "tie_breaker": 0.8
   }
 }
}
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 7,
      "relation": "eq"
    },
    "max_score": 1.890432,
    "hits": [
      {
        "_index": "products",
        "_id": "1",
        "_score": 1.890432,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "high-rise red jeans",
          "embedding": [
            1,
            1,
            1,
            1
          ],
          "price": 100
        }
      },
      {
        "_index": "products",
        "_id": "2",
        "_score": 1.890432,
        "_source": {
          "department": "women",
          "brand": "Calvin Klein",
          "description": "high-rise beautiful jeans",
          "embedding": [
            1,
            1,
            1,
            1
          ],
          "price": 250
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 1.0679927,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 75
        },
        "matched_queries": [
          "knn_query"
        ]
      },
      {
        "_index": "products",
        "_id": "6",
        "_score": 1.0679927,
        "_source": {
          "department": "men",
          "brand": "Levi's",
          "description": "jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 50
        },
        "matched_queries": [
          "knn_query"
        ]
      },
      {
        "_index": "products",
        "_id": "5",
        "_score": 1.0556482,
        "_source": {
          "department": "women",
          "brand": "Levi's",
          "description": "luxury jeans",
          "embedding": [
            2,
            2,
            2,
            0
          ],
          "price": 150
        },
        "matched_queries": [
          "knn_query"
        ]
      }
    ]
  }
}

kNN 搜索作为查询已在 8.12 版本中引入。 请尝试一下,如果有任何反馈,我们将不胜感激。

原文:Introducing kNN query, an expert way to do kNN search — Elastic Search Labs文章来源地址https://www.toymoban.com/news/detail-817615.html

到了这里,关于Elasticsearch:介绍 kNN query,这是进行 kNN 搜索的专家方法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Elasticsearch:探索 k-nearest neighbor (kNN) 搜索

    由于新一代机器学习模型可以将各种内容表示为向量,包括文本、图像、事件等,人们对向量搜索的兴趣激增。 通常称为 “ 嵌入模型 (embedding models)”,这些强大的表示可以以超越其表面特征的方式捕获两段内容之间的相似性。 K 最近邻 (KNN) 搜索又名语义搜索是一种简单

    2024年02月08日
    浏览(40)
  • 增强FAQ搜索引擎:发挥Elasticsearch中KNN的威力

    英文原文地址:https://medium.com/nerd-for-tech/enhancing-faq-search-engines-harnessing-the-power-of-knn-in-elasticsearch-76076f670580 增强FAQ搜索引擎:发挥Elasticsearch中KNN的威力 2023 年 10 月 21 日 在一个快速准确的信息检索至关重要的时代,开发强大的搜索引擎是至关重要的。随着大型语言模型(LLM)和

    2024年02月02日
    浏览(37)
  • elasticsearch 笔记二:搜索DSL 语法(搜索API、Query DSL)

    从索引 tweet 里面搜索字段 user 为 kimchy 的记录 从索引 tweet,user 里面搜索字段 user 为 kimchy 的记录 从所有索引里面搜索字段 tag 为 wow 的记录 说明:搜索的端点地址可以是多索引多 mapping type 的。搜索的参数可作为 URI 请求参数给出,也可用 request body 给出 URI 搜索方式通过 URI

    2024年02月04日
    浏览(55)
  • 增强常见问题解答搜索引擎:在 Elasticsearch 中利用 KNN 的力量

    在快速准确的信息检索至关重要的时代,开发强大的搜索引擎至关重要。 随着大型语言模型和信息检索架构(如 RAG)的出现,在现代软件系统中利用文本表示(向量/嵌入)和向量数据库已变得越来越流行。 在本文中,我们深入研究了如何使用 Elasticsearch 的 K 最近邻 (KNN) 搜

    2024年02月08日
    浏览(53)
  • Elasticsearch Boolean Query查询介绍

    前言 ES 和 Solr 的底层都是基于Apache Lucene 实现,bool 查询的底层实现是Lucene 的 BooleanQuery,其可以组合多个子句查询,类似 SQL 语句里面的 OR 查询。 查询介绍 在 ES 里面 Boolean 查询封装了 4 种 API 接口能力,可以单独使用,也可以组合使用,总结如下: 函数 描述 must query 关键

    2024年02月13日
    浏览(49)
  • Elasticsearch实战(十五)---查询query,filter过滤,结合aggs 进行局部/全局聚合统计

    Elasticsearch实战-查询query,filter过滤,结合aggs 进行局部/全局聚合统计 1.准备数据 2. ES 查询query,filter过滤,结合aggs 聚合统计 2.1 查询命中后,基于查询的数据进行聚合 前面我们讲的所有的聚合操作 都是没有查询的,都是上来直接 aggs 进行 聚合 avg, count, 如果现在我想统计

    2024年02月10日
    浏览(58)
  • Elasticsearch:使用 Elasticsearch 进行语义搜索

    在数字时代,搜索引擎在通过浏览互联网上的大量可用信息来检索数据方面发挥着重要作用。 此方法涉及用户在搜索栏中输入特定术语或短语,期望搜索引擎返回与这些确切匹配的结果。 虽然搜索对于简化信息检索非常有价值,但它也有其局限性。 主要缺点之

    2024年02月08日
    浏览(51)
  • Elasticsearch:使用 Transformers 和 Elasticsearch 进行语义搜索

    什么语义搜索( semantic search )呢?根据搜索查询的意图和上下文含义(而不仅仅是)检索结果。语义/向量搜索是一种强大的技术,可以大大提高搜索结果的准确性和相关性。 与传统的基于的搜索方法不同,语义搜索使用单词的含义和上下文来理解查询背后的意

    2024年02月08日
    浏览(57)
  • Elasticsearch:使用 ELSER 进行语义搜索

    Elastic Learned Sparse EncodeR(或 ELSER)是一种由 Elastic 训练的 NLP 模型,使你能够使用稀疏向量表示来执行语义搜索。 语义搜索不是根据搜索词进行字面匹配,而是根据搜索查询的意图和上下文含义检索结果。 本教程中的说明向你展示了如何使用 ELSER 对数据执行语义搜索。 提示

    2024年02月11日
    浏览(50)
  • Elasticsearch:使用 fuzziness 来进行搜索

    在我之前的文章 “Elasticsearch:fuzzy 搜索 (模糊搜索)”,我详细描述了模糊搜索。尽管那篇文章已经很详尽了,但是还是有 auto 这个配置没有完全覆盖到。在今天的文章中,我们来进一步对这个进行讲解一下。 Fuzziness 参数存在于某些查询中,使用它时,你将受益于根据术

    2024年02月08日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包