C#, 고성능 Tag Search
태그(tag) 조회 성능을 위해 Aho-Corasick 알고리즘을 사용하는데 이를 위해 .NET 9 이상에서 제공하는 SearchValues<string> 함수를 이용하는 예제이다.
최적화 핵심은 그룹화, 길이 내림차순 정렬, FrozenDictionary (읽기 전용 최적화 컬렉션) 자료구조이다. IndexOfAny로 특정 단어로 시작하는 단어들만 루프를 돌게 한다. Greedy Matching (긴 단어 우선), ReadOnlySpan<char>으로 메모리(GC) 최적화 등을 포함한다.
성능을 위해 Parallel.ForEach로 문서를 문단 단위로 나누어 병렬처리할 수 있다.
유의어(Alias) 사전 운영은 {"ID": 1, "Name": "LG", "Aliases": ["LG전자", "LG디스플레이", "LGCNS", "LG CNS"]}으로 관리하고, SearchValues 엔진에는 Aliases에 있는 모든 단어를 다 때려 넣고 무엇이 걸리든 결과는 "ID": 1(LG)로 리턴하게 만드는 방식을 사용한다.
Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ConsoleApp;
internal class Program
{
private static async Task Main()
{
// tag dictionary는 DB에서 불러온다고 가정
string[] myDictionary =
["삼성전자", "현대차", "LG", "SK하이닉스", "C#", ".NET", "삼성"];
const string document = "삼성전자는 이번 분기 실적 발표에서 현대차와의 협업을 발표했습니다. 또한 C#과 NET 환경에서의 소프트웨어 최적화가 중요해지고 있으며, SK하이닉스의 반도체 기술도 언급되었습니다.";
// ~ 10,000 태그 가정
TagProcessor tagProcessor = new(myDictionary);
List<string> tags = tagProcessor.ExtractAllTags(document);
Console.WriteLine($"추출된 태그 개수 : {tags.Count}");
foreach (string tag in tags)
{
Console.WriteLine($"- {tag}");
}
Console.WriteLine("----------------------------");
// 10,000 ~ 100,000개 태그 가정
Console.WriteLine("태그 추출 시작 ...");
List<string> extractTags = await Task.Run(() =>
{
HighDensityTagProcessor hightProcessor = new(myDictionary);
return hightProcessor.ExtractTags(document);
});
Console.WriteLine($"추출된 태그 개수(High) : {extractTags.Count}");
foreach (string tag in extractTags)
{
Console.WriteLine($"- {tag}");
}
Console.WriteLine("태그 추출 완료");
}
}