-
Meilisearch: Rust로 작성된 ElasticSearch컴퓨터/Rust 2024. 7. 12. 16:45728x90반응형
[메일리서치] meili는 노르드 신화 (Norse god)에 나오는 신 이름, "사랑스러운 사람"을 뜻
https://github.com/meilisearch/MeiliSearch/issues/1182Rust언어로 작성된 오픈 소스 검색 엔진이다.
이 링크에서 다른 검색 엔진들과 비교가 있는데 정리하면
기능 Meilisearch Elasticsearch Algolia Typesense 라이선스 MIT SSPL (완전 오픈 소스 X) 폐쇄 GPL-3 언어 Rust Java C++ C++ 저장 방식 디스크 + 메모리 매핑 (RAM 제한 없음) 디스크 + RAM 캐시 RAM 제한 RAM 제한 설정 복잡성 간단, 필요 없음 복잡, 광범위 설정 간단, 상용 간단, 최소 설정 오타 관용성 내장, 자동 fuzzy 쿼리 매개변수 필요 내장, 자동 내장, 자동 검색 속도 < 50ms 가변 < 10ms < 50ms 분산 처리, 고급 분석, 이미 있는 생태계를 보면 Elasticsearch가 좋지만,
간단하고 빠른 쓸만한 검색기를 쓸 때는 meilisearch도 좋아 보인다.
고맙게도 lindera + 사전을 이용해 일본어/중국어 (CJK), 한국어를 지원한다. @인덱서 Github
예제 (Go언어)
부분 예제로 보여줄 것은 TJ 노래방에 있는 노래/가사 검색기이다. (약 5만 개의 곡과 4만 개의 가사 인덱싱)
위 링크에서 내가 사용하고 싶은 프로그래밍 언어가 지원되는지, 라이브러리 설치법을 읽어준다.
Key
마스터 키가 있고, 특정 인덱스에서 검색만 하게 할 수 있는 API 키를 만들 수 있다.
https://www.meilisearch.com/docs/reference/api/keys#create-a-key
client.CreateKey(&meilisearch.Key{ Description: "products 인덱스에 문서 추가 및 검색 가능 (2042년 만료)", Actions: []string{"documents.add", "search"}, Indexes: []string{"products"}, ExpiresAt: time.Date(2042, time.April, 02, 0, 42, 42, 0, time.UTC), })
Healthcheck
그 후 아래와 같이 health check를 할 수 있다.
func main() { client := meilisearch.NewClient(meilisearch.ClientConfig{ Host: "메일리서치 검색 서버", APIKey: "key", }) // Check connection to Meilisearch health, err := client.Health() if err != nil { fmt.Println("Error connecting to Meilisearch:", err) os.Exit(1) } fmt.Println("Meilisearch is healthy:", health.Status == "available") }
동의어 (Synonym)
HashMap<String, String[]> synonyms = new HashMap<String, String[]>(); synonyms.put("great", new String[] {"fantastic"}); synonyms.put("fantastic", new String[] {"great"}); client.index("movies").updateSynonymsSettings(synonyms);
"뮤즈"를 검색해도 "Muse" 검색 결과가 나올 수 있게 하고,
"브루노 마스", "마스"를 검색해도 "Bruno Mars" 검색 결과가 나올 수 있게 해주는 설정
map[string][]string 형식으로 만들어 주면 된다. (아래는 txt 파일에서 읽는 함수)
// Function to read the synonyms from a file func loadSynonyms(filePath string) (map[string][]string, error) { synonyms := make(map[string][]string) file, err := os.Open(filePath) if err != nil { return nil, err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() parts := strings.Split(line, ":") if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) values := strings.Split(strings.TrimSpace(parts[1]), ",") for i := range values { values[i] = strings.TrimSpace(values[i]) } synonyms[key] = values } if err := scanner.Err(); err != nil { return nil, err } return synonyms, nil }
인덱스
설정은 공식 문서에서 읽어보면서 제일 만족스러운 결과를 찾으면 된다.
RankingRules에 따라 검색 결과가 많이 달라질 수 있다. 자세한 spec 참고 @링크
func indexSong(client *meilisearch.Client) { synonyms, err := loadSynonyms("synonyms.txt") // 동의어 불러오기 if err != nil { fmt.Printf("Error loading synonyms: %v\n", err) return } _, err = client.DeleteIndex("songs") if err != nil { fmt.Println("Error deleting index:", err) os.Exit(1) } // 기본 키 이름 _, err = client.CreateIndex(&meilisearch.IndexConfig{ Uid: "songs", PrimaryKey: "id", }) if err != nil { fmt.Println("Error creating index:", err) os.Exit(1) } index := client.Index("songs") settings := meilisearch.Settings{ FilterableAttributes: []string{"facet 필드"}, // "lyricist", "composer" SearchableAttributes: []string{"검색 가능할 필드"}, RankingRules: []string{ "랭킹 순서 정할 수 있음" }, TypoTolerance: &meilisearch.TypoTolerance{ Enabled: true, MinWordSizeForTypos: meilisearch.MinWordSizeForTypos{ OneTypo: 6, // 한국어에서 어떤 식으로 작동하는지 모르겠음 TwoTypos: 9, // https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#minwordsizefortypos }, DisableOnWords: []string{}, DisableOnAttributes: []string{"오타 허용하고 싶지 않은 필드"}, }, Synonyms: synonyms, } _, err = index.UpdateSettings(&settings) if err != nil { fmt.Println("Error setting index settings:", err) os.Exit(1) } songs, err := getSongs("songs.json") // JSON에서 불러옴 if err != nil { fmt.Println("Error reading JSON:", err) os.Exit(1) } // 100개씩 배치 인덱스 taskList, err := index.AddDocumentsInBatches(songs, 100, "id") if err != nil { fmt.Println("Error adding documents in batches:", err) os.Exit(1) } // 배치를 안하면 많은 수의 doc을 인덱싱할 때 느릴 수 있음 for _, task := range taskList { fmt.Println("Task UID:", task.TaskUID) // Check the task status taskStatus, err := client.GetTask(task.TaskUID) if err != nil { fmt.Println("Error retrieving task status:", err) os.Exit(1) } if taskStatus.Status == "failed" { fmt.Printf("Task failed with error: %s\n", taskStatus.Error) } else { fmt.Printf("Task Status: %+v\n", taskStatus) } } }
검색
기본 검색은 쉽다, 그냥 불러주면 웬만한 설정이 다 되어있다.
func searchSong(client *meilisearch.Client, query string) { result, err := client.Index("songs").Search(query, &meilisearch.SearchRequest{ MatchingStrategy: "all", }) if err != nil { log.Fatalf("Search error: %v", err) } hits := result.Hits if len(hits) == 0 { log.Println("No hits found") return } // 10개 검색 결과 for i := 0; i < len(hits) && i < 10; i++ { hit, ok := hits[i].(map[string]interface{}) if !ok { log.Printf("Unexpected hit format: %v", hits[i]) continue } number, numberOk := hit["id"].(string) title, titleOk := hit["title"].(string) singer, singerOk := hit["singer"].(string) if !numberOk || !titleOk || !singerOk { log.Printf("Incomplete data for hit: %v", hit) continue } log.Printf("%s: %s - %s", number, title, singer) } }
728x90'컴퓨터 > Rust' 카테고리의 다른 글
Rust: 스타크래프트1 멀티 실행기 만들기 (windows api) (0) 2024.04.22 Rust: 비동기 리팩토링 tokio::task::JoinSet (0) 2024.04.06 Rust: OAuth2 구글, Github, 카카오, 네이버 로그인 (1) 2023.08.06