C# NEST Elastic Search 多条件查询

C# NEST Elastic Search Query Multiple Conditions

我正在做一些测试来改变我的架构。我们想放弃 MongoDB 并改用 ElasticSearch。但我真的不知道这项技术。我正在使用 NEST 作为驱动程序,无法翻译我曾经在 mongo.

中使用的查询
public async Task<IEnumerable<Keyword>> GetKeywordsAsync(string prefix, int startIndex, int totalItems, int minimumTotalSearch, CancellationToken cancellationToken)
    {
        return await _mongoReader.GetEntitiesAsync<KeywordEntity, Keyword>(CollectionName,
                    queryable =>
                        queryable.Where(entity => entity.KeywordName.StartsWith(prefix) && entity.TotalSearch >= minimumTotalSearch)
                                 .OrderBy(entity => entity.KeywordName)
                                 .Select(_keywordConverter.GetConverter())
                                 .Skip(startIndex)
                                 .Take(totalItems),
                    cancellationToken).ConfigureAwait(false);
    }

public async Task<IEnumerable<TModel>> GetEntitiesAsync<TDocument, TModel>(string collectionName,
            Func<IMongoQueryable<TDocument>, IMongoQueryable<TModel>> getQueryable,
            CancellationToken cancellationToken)
        {
            var documents = GetDocuments<TDocument>(collectionName);
            var query = getQueryable(documents.AsQueryable());
            return await query.ToListAsync(cancellationToken).ConfigureAwait(false);
        }

这是我为 ElasticSearch 做的一个简单发现:

public async Task<IEnumerable<TModel>> FindAsync<TModel, TValue>(string index,
        Expression<Func<TModel, TValue>> findExpression, TValue value, int limit,
        CancellationToken cancellationToken) where TModel : class
    {
        var searchRequest = new SearchRequest<TModel>(index)
        {
            Query =
                Query<TModel>.Match(
                    a => a.Field(findExpression).Query(string.Format(CultureInfo.InvariantCulture, "{0}", value))),
            Size = limit
        };

        var resGet = await _elasticClientFactory.Create().SearchAsync<TModel>(searchRequest, cancellationToken).ConfigureAwait(false);

        return resGet?.Documents;
    }

问题是我无法在 Elastic 中翻译我的查询 Mongo ...

这很痛苦,但这是弹性查询:

{
  "query": {
    "bool": {
      "must": [
        {"range" : { "totalSearch" : { "gte" : minimumTotalSearch }}},
        {"prefix": { "keywordName": prefix}}
      ]
    }
  },
  "from": startIndex,
  "size": totalItems
}

--> 解决方案: 经过一番努力编码后,我找到了一种在 C# 中进行查询的方法:

var result =
            ecf.Create()
                .Search<KeywordEntity>(
                    a => a.Query(
                        z =>
                            z.Bool(
                                e =>
                                    e.Must(r => r.Range(t => t.Field(y => y.TotalSearch).GreaterThanOrEquals(minimumTotalSearch)),
                                        t => t.Prefix(y => y.KeywordName, prefix)))).Index("keywords"));

但现在我问自己这是否是执行此查询的最佳方法(没有 skip/take 这很简单)。因为我是新手,所以可能有一个更优化的查询 ...

查询应该是这样的。

client.Search<KeywordEntity>(s => s.Index("<INDEX NAME>")
                                    .Type("<TYPE NAME>")
                                    .Query(q =>q
                                        .Bool(b => b.
                                            Must(prefix => prefix.Prefix(pre => pre.OnField("KeywordName").Value("PREFIX QUERY")))
                                            .Must(range => range.Range(ran => ran.OnField("TotalSearch").GreaterOrEquals(minimumTotalSearch)))
                          )).SortAscending("KeywordName")
                          .From(StartIndex)
                          .Size(totalItems));

如果您发现任何困难,请告诉我。

您的解决方案看起来不错,但有几点值得强调。

  1. 客户端为thread-safe,大量使用缓存,建议创建单例并复用;不这样做将意味着需要根据每个请求重建缓存,从而降低性能。
  2. 由于 range 查询查找匹配或不匹配的文档,即它是一个不需要对匹配文档进行评分的谓词,因此 range 查询可以包含在bool查询filter子句; Elasticsearch 可以使用 roaring bitmaps.
  3. 缓存这些子句

NEST 还重载了 QueryContainer(根查询类型)上的运算符作为 shorthand 以组合它们以构建 bool 查询。然后你的解决方案就可以变成(加上上面的建议)

var searchResponse = client.Search<KeywordEntity>(s => s
    .Index("keywords")
    .Query(q => q
        .Prefix(p => p.KeywordName, prefix) && +q
        .Range(r => r
            .Field(y => y.TotalSearch)
            .GreaterThanOrEquals(minimumTotalSearch)
        )
    )
);

您可以使用 .From().Size()(别名分别为 .Skip().Take())进行分页,并指定仅返回部分字段集来自使用 source filtering 的来源。一个更完整的例子是

var client = new ElasticClient();

var minimumTotalSearch = 10;
var prefix = "prefix";
var startIndex = 10;
var totalItems = 10;

var searchResponse = client.Search<KeywordEntity>(s => s
    .Index("keywords")
    .Query(q => q
        .Prefix(p => p.KeywordName, prefix) && +q
        .Range(r => r
            .Field(y => y.TotalSearch)
            .GreaterThanOrEquals(minimumTotalSearch)
        )
    )
    // source filtering
    .Source(sf => sf
        .Includes(f => f
            .Fields(
                ff => ff.KeywordName,
                ff => ff.TotalSearch
            )
        )
    )
    // sorting. By default, documents will be sorted by _score descending
    .Sort(so => so
        .Ascending(a => a.KeywordName)
    )
    // skip x documents
    .Skip(startIndex)
    // take next y documents
    .Take(totalItems)
);

这将构建查询

{
  "from": 10,
  "size": 10,
  "sort": [
    {
      "keywordName": {
        "order": "asc"
      }
    }
  ],
  "_source": {
    "includes": [
      "keywordName",
      "totalSearch"
    ]
  },
  "query": {
    "bool": {
      "must": [
        {
          "prefix": {
            "keywordName": {
              "value": "prefix"
            }
          }
        }
      ],
      "filter": [
        {
          "range": {
            "totalSearch": {
              "gte": 10.0
            }
          }
        }
      ]
    }
  }
}

最后一件事 :) 因为在你的 Mongo 查询中,你是按前缀升序排序的,你也可以放弃在 Elasticsearch 查询中对 prefix 查询进行评分,方法是将其设为 [= bool 查询中的 16=] 子句。