📖 数据密集型设计

全文检索优化与实践

深入探讨全文检索优化与实践技术

一、全文检索概述

全文检索是一种从大量文本数据中快速查找相关信息的技术。在数据密集型应用中,全文检索是提升用户体验和数据价值的关键技术。

二、全文检索原理

2.1 倒排索引原理

倒排索引是全文检索的核心数据结构:

graph TD A[文档集合] --> B[分词处理] B --> C[文档1: '数据密集型应用设计'] C --> C1[数据] C --> C2[密集型] C --> C3[应用] C --> C4[设计] B --> D[文档2: '分布式系统架构'] D --> D1[分布式] D --> D2[系统] D --> D3[架构] B --> E[文档3: '数据存储技术'] E --> E1[数据] E --> E2[存储] E --> E3[技术] C1 & E1 --> F[数据: 文档1, 文档3] C2 --> G[密集型: 文档1] C3 --> H[应用: 文档1] C4 --> I[设计: 文档1] D1 --> J[分布式: 文档2] D2 --> K[系统: 文档2] D3 --> L[架构: 文档2] E2 --> M[存储: 文档3] E3 --> N[技术: 文档3] F & G & H & I & J & K & L & M & N --> O[倒排索引]

2.2 倒排索引结构

组件 描述 作用
词典 所有唯一的词项 快速查找词项
倒排列表 包含词项的文档列表 定位包含词项的文档
词频 词项在文档中的出现次数 计算相关性
位置信息 词项在文档中的位置 支持短语查询

三、分词技术

3.1 分词算法对比

算法 原理 优点 缺点 适用语言
基于词典 匹配词典中的词 准确率高 新词识别差 中文
基于统计 基于词频统计 支持新词 准确率低 中文
基于规则 基于语法规则 结构化文本效果好 通用性差 任意语言
N-gram 滑动窗口切分 简单高效 索引量大 任意语言

3.2 中文分词实现

public class ChineseTokenizer
{
    private readonly Dictionary<string, int> _dictionary;
    private const int MaxWordLength = 6;
    
    public ChineseTokenizer(string dictionaryPath)
    {
        _dictionary = LoadDictionary(dictionaryPath);
    }
    
    private Dictionary<string, int> LoadDictionary(string path)
    {
        var dict = new Dictionary<string, int>();
        
        foreach (var line in File.ReadLines(path))
        {
            var parts = line.Split('\t');
            if (parts.Length >= 2)
            {
                dict[parts[0]] = int.Parse(parts[1]);
            }
        }
        
        return dict;
    }
    
    public List<string> Tokenize(string text)
    {
        var tokens = new List<string>();
        int i = 0;
        
        while (i < text.Length)
        {
            string token = FindLongestMatch(text, i);
            
            if (token != null)
            {
                tokens.Add(token);
                i += token.Length;
            }
            else
            {
                tokens.Add(text[i].ToString());
                i++;
            }
        }
        
        return tokens;
    }
    
    private string FindLongestMatch(string text, int start)
    {
        for (int length = Math.Min(MaxWordLength, text.Length - start); length >= 1; length--)
        {
            var candidate = text.Substring(start, length);
            
            if (_dictionary.ContainsKey(candidate))
            {
                return candidate;
            }
        }
        
        return null;
    }
}

四、Elasticsearch优化

4.1 索引设计优化

public class ElasticsearchIndexOptimizer
{
    public async Task CreateOptimizedIndexAsync(string indexName, IndexSettings settings)
    {
        var createIndexRequest = new CreateIndexRequest(indexName)
        {
            Settings = new IndexSettings
            {
                NumberOfShards = settings.NumberOfShards,
                NumberOfReplicas = settings.NumberOfReplicas,
                Analysis = CreateAnalysisSettings()
            },
            Mappings = CreateMappings(settings.Fields)
        };
        
        await _elasticsearchClient.Indices.CreateAsync(createIndexRequest);
    }
    
    private Analysis CreateAnalysisSettings()
    {
        return new Analysis
        {
            Analyzers = new Analyzers
            {
                { "custom_analyzer", new CustomAnalyzer
                {
                    Tokenizer = "ik_max_word",
                    Filter = new List<string> { "lowercase", "stop", "synonym" }
                }}
            },
            Filters = new TokenFilters
            {
                { "stop", new StopTokenFilter { Stopwords = "_english_" } },
                { "synonym", new SynonymTokenFilter
                {
                    Synonyms = new List<string>
                    {
                        "数据密集型,大数据",
                        "分布式系统,分布式架构"
                    }
                }}
            }
        };
    }
    
    private TypeMapping CreateMappings(List<IndexField> fields)
    {
        var properties = new Properties();
        
        foreach (var field in fields)
        {
            properties.Add(field.Name, CreateProperty(field));
        }
        
        return new TypeMapping { Properties = properties };
    }
    
    private IProperty CreateProperty(IndexField field)
    {
        return field.Type switch
        {
            FieldType.Text => new TextProperty
            {
                Analyzer = "custom_analyzer",
                SearchAnalyzer = "custom_analyzer",
                Fields = new Properties
                {
                    { "keyword", new KeywordProperty { IgnoreAbove = 256 } }
                }
            },
            FieldType.Keyword => new KeywordProperty(),
            FieldType.Numeric => new LongProperty(),
            FieldType.Date => new DateProperty { Format = "yyyy-MM-dd HH:mm:ss" },
            _ => new TextProperty()
        };
    }
}

4.2 查询优化

public class ElasticsearchQueryOptimizer
{
    public async Task<SearchResponse<T>> SearchAsync<T>(string indexName, QueryRequest request)
    {
        var searchRequest = new SearchRequest(indexName)
        {
            Query = BuildQuery(request),
            Sort = BuildSort(request),
            From = request.From,
            Size = request.Size,
            _Source = request.IncludeFields,
            TrackTotalHits = true
        };
        
        return await _elasticsearchClient.SearchAsync<T>(searchRequest);
    }
    
    private QueryContainer BuildQuery(QueryRequest request)
    {
        var query = new QueryContainer();
        
        if (!string.IsNullOrEmpty(request.Keyword))
        {
            query &= new MultiMatchQuery
            {
                Query = request.Keyword,
                Fields = new[] { "title^3", "content^2", "description" },
                Operator = Operator.And,
                Fuzziness = Fuzziness.Auto
            };
        }
        
        if (request.Filters != null)
        {
            foreach (var filter in request.Filters)
            {
                query &= new TermQuery { Field = filter.Key, Value = filter.Value };
            }
        }
        
        if (request.DateRange != null)
        {
            query &= new DateRangeQuery
            {
                Field = "created_at",
                GreaterThanOrEqualTo = request.DateRange.Start,
                LessThanOrEqualTo = request.DateRange.End
            };
        }
        
        return query;
    }
    
    private List<ISort> BuildSort(QueryRequest request)
    {
        var sorts = new List<ISort>();
        
        if (!string.IsNullOrEmpty(request.SortField))
        {
            sorts.Add(new FieldSort
            {
                Field = request.SortField,
                Order = request.SortOrder == SortOrder.Descending 
                    ? SortOrder.Descending 
                    : SortOrder.Ascending
            });
        }
        
        sorts.Add(new ScoreSort());
        
        return sorts;
    }
}

4.3 查询缓存优化

public class ElasticsearchCacheOptimizer
{
    public async Task<SearchResponse<T>> SearchWithCacheAsync<T>(string indexName, QueryRequest request, TimeSpan cacheDuration)
    {
        var cacheKey = GenerateCacheKey(request);
        
        var cachedResponse = await _cache.GetAsync(cacheKey);
        
        if (cachedResponse != null)
        {
            return JsonSerializer.Deserialize<SearchResponse<T>>(cachedResponse);
        }
        
        var response = await _queryOptimizer.SearchAsync<T>(indexName, request);
        
        await _cache.SetAsync(cacheKey, JsonSerializer.Serialize(response), 
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = cacheDuration
            });
        
        return response;
    }
    
    private string GenerateCacheKey(QueryRequest request)
    {
        return $"es:query:{HashHelper.ComputeHash(JsonSerializer.Serialize(request))}";
    }
}

五、相关性算法

5.1 TF-IDF算法

public class TfIdfAlgorithm
{
    public Dictionary<string, double> CalculateTfIdf(string document, List<string> allDocuments)
    {
        var tfScores = CalculateTf(document);
        var idfScores = CalculateIdf(document, allDocuments);
        
        var tfIdfScores = new Dictionary<string, double>();
        
        foreach (var term in tfScores.Keys)
        {
            tfIdfScores[term] = tfScores[term] * idfScores.GetValueOrDefault(term, 0);
        }
        
        return tfIdfScores;
    }
    
    private Dictionary<string, double> CalculateTf(string document)
    {
        var tokens = _tokenizer.Tokenize(document);
        var totalTerms = tokens.Count;
        var termCounts = tokens.GroupBy(t => t).ToDictionary(g => g.Key, g => g.Count());
        
        return termCounts.ToDictionary(kv => kv.Key, kv => kv.Value / (double)totalTerms);
    }
    
    private Dictionary<string, double> CalculateIdf(string document, List<string> allDocuments)
    {
        var tokens = _tokenizer.Tokenize(document);
        var uniqueTerms = tokens.Distinct().ToList();
        
        var idfScores = new Dictionary<string, double>();
        
        foreach (var term in uniqueTerms)
        {
            var documentCount = allDocuments.Count(d => d.Contains(term));
            idfScores[term] = Math.Log(allDocuments.Count / (double)(documentCount + 1));
        }
        
        return idfScores;
    }
}

5.2 BM25算法

public class Bm25Algorithm
{
    private readonly double _k1 = 1.2;
    private readonly double _b = 0.75;
    
    public Dictionary<string, double> CalculateBm25(string document, List<string> allDocuments)
    {
        var tfScores = CalculateTf(document);
        var idfScores = CalculateIdf(document, allDocuments);
        var averageDocumentLength = CalculateAverageDocumentLength(allDocuments);
        var documentLength = _tokenizer.Tokenize(document).Count;
        
        var bm25Scores = new Dictionary<string, double>();
        
        foreach (var term in tfScores.Keys)
        {
            var tf = tfScores[term];
            var idf = idfScores.GetValueOrDefault(term, 0);
            
            var numerator = tf * (_k1 + 1);
            var denominator = tf + _k1 * (1 - _b + _b * documentLength / averageDocumentLength);
            
            bm25Scores[term] = idf * numerator / denominator;
        }
        
        return bm25Scores;
    }
    
    private double CalculateAverageDocumentLength(List<string> documents)
    {
        return documents.Average(d => _tokenizer.Tokenize(d).Count);
    }
}

六、全文检索性能优化

6.1 性能优化策略

优化方向 具体措施 预期效果
索引优化 合理分片、副本数、字段类型 提升写入和查询性能
查询优化 使用filter、避免*查询、限制返回字段 降低查询延迟
缓存优化 缓存热点查询、使用query cache 减少计算开销
硬件优化 SSD存储、足够内存、多核CPU 提升整体性能

6.2 查询性能分析

public class SearchPerformanceAnalyzer
{
    public async Task<PerformanceAnalysis> AnalyzeAsync(string indexName, QueryRequest request, int sampleCount = 100)
    {
        var responseTimes = new List<long>();
        
        for (int i = 0; i < sampleCount; i++)
        {
            var stopwatch = Stopwatch.StartNew();
            await _elasticsearchClient.SearchAsync<object>(indexName, 
                s => s.Query(q => q.MatchAll()));
            stopwatch.Stop();
            
            responseTimes.Add(stopwatch.ElapsedMilliseconds);
        }
        
        return new PerformanceAnalysis
        {
            AverageResponseTimeMs = responseTimes.Average(),
            MaxResponseTimeMs = responseTimes.Max(),
            MinResponseTimeMs = responseTimes.Min(),
            P95ResponseTimeMs = responseTimes.OrderBy(t => t).ElementAt((int)(sampleCount * 0.95)),
            P99ResponseTimeMs = responseTimes.OrderBy(t => t).ElementAt((int)(sampleCount * 0.99))
        };
    }
    
    public async Task<string> ExplainQueryAsync(string indexName, QueryRequest request)
    {
        var explainRequest = new ExplainRequest(indexName, "_all")
        {
            Query = _queryOptimizer.BuildQuery(request)
        };
        
        var response = await _elasticsearchClient.ExplainAsync(explainRequest);
        
        return response.ToString();
    }
}

七、全文检索监控

7.1 监控指标

public class SearchMetrics
{
    public string IndexName { get; set; }
    public int DocumentCount { get; set; }
    public long IndexSizeBytes { get; set; }
    public int QueryCount { get; set; }
    public double AverageResponseTimeMs { get; set; }
    public double P95ResponseTimeMs { get; set; }
    public double P99ResponseTimeMs { get; set; }
    public int CacheHitRate { get; set; }
}

public class SearchMonitor
{
    public async Task<SearchMetrics> GetMetricsAsync(string indexName)
    {
        var stats = await _elasticsearchClient.Indices.StatsAsync(indexName);
        var queryStats = await _elasticsearchClient.Nodes.StatsAsync();
        
        return new SearchMetrics
        {
            IndexName = indexName,
            DocumentCount = stats.Indices[indexName].Primaries.Docs.Count,
            IndexSizeBytes = stats.Indices[indexName].Primaries.Store.SizeInBytes,
            QueryCount = queryStats.Nodes.Sum(n => n.Search.Total),
            AverageResponseTimeMs = queryStats.Nodes.Average(n => n.Search.AverageTimeInMillis),
            CacheHitRate = CalculateCacheHitRate(queryStats)
        };
    }
    
    private int CalculateCacheHitRate(NodesStatsResponse stats)
    {
        var totalQueries = stats.Nodes.Sum(n => n.Search.Total);
        var cacheHits = stats.Nodes.Sum(n => n.Search.QueryCache.HitCount);
        
        return totalQueries > 0 ? (int)(cacheHits / (double)totalQueries * 100) : 0;
    }
    
    public async Task MonitorAsync()
    {
        var metrics = await GetMetricsAsync("articles");
        
        if (metrics.AverageResponseTimeMs > 1000)
        {
            await _alertService.SendAlert("搜索响应时间过高", 
                $"平均响应时间: {metrics.AverageResponseTimeMs}ms");
        }
        
        if (metrics.CacheHitRate < 50)
        {
            await _alertService.SendAlert("缓存命中率过低", 
                $"缓存命中率: {metrics.CacheHitRate}%");
        }
    }
}

八、全文检索最佳实践

8.1 选择合适的分词器

根据语言和业务需求选择合适的分词器。

8.2 合理设计索引

根据查询模式设计索引结构,选择合适的字段类型。

8.3 使用filter过滤

对于不需要评分的条件,使用filter进行过滤。

8.4 限制返回字段

只返回需要的字段,减少数据传输。

8.5 监控查询性能

定期分析查询性能,优化慢查询。

九、总结

全文检索是数据密集型应用中提升用户体验的关键技术。通过理解倒排索引原理、选择合适的分词算法、优化Elasticsearch配置和查询,能够构建高性能的全文检索系统。相关性算法(TF-IDF、BM25)能够提升搜索结果的准确性,缓存策略能够减少查询延迟。监控和调优是持续优化的关键。