ABOUT ME

-

Total
-
  • Meilisearch: Rust로 작성된 ElasticSearch
    컴퓨터/Rust 2024. 7. 12. 16:45
    728x90
    반응형
    [메일리서치] meili는 노르드 신화 (Norse god)에 나오는 신 이름, "사랑스러운 사람"을 뜻

    https://github.com/meilisearch/MeiliSearch/issues/1182

     

     

    GitHub - meilisearch/meilisearch: A lightning-fast search API that fits effortlessly into your apps, websites, and workflow

    A lightning-fast search API that fits effortlessly into your apps, websites, and workflow - meilisearch/meilisearch

    github.com

     

    Rust언어로 작성된 오픈 소스 검색 엔진이다.

    이 링크에서 다른 검색 엔진들과 비교가 있는데 정리하면

    기능 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

     

    GitHub - meilisearch/charabia: Library used by Meilisearch to tokenize queries and documents

    Library used by Meilisearch to tokenize queries and documents - meilisearch/charabia

    github.com

     

    예제 (Go언어)

    부분 예제로 보여줄 것은 TJ 노래방에 있는 노래/가사 검색기이다. (약 5만 개의 곡과 4만 개의 가사 인덱싱)

     

    TJ미디어 노래 검색

    TJ 노래방 노래를 검색해보세요.

    my-karaoke.github.io

     

     

     

    Official SDKs and libraries — Meilisearch documentation

    Our team and community have worked hard to bring Meilisearch to almost all popular web development languages, frameworks, and deployment options. New integrations are constantly in development. If you'd like to contribute, see below. You can use Meilisearc

    www.meilisearch.com

    위 링크에서 내가 사용하고 싶은 프로그래밍 언어가 지원되는지, 라이브러리 설치법을 읽어준다.

     

    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
    }

    synonyms.txt 형식

     

    인덱스

    설정은 공식 문서에서 읽어보면서 제일 만족스러운 결과를 찾으면 된다.

    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)
    		}
    	}
    }

     

    검색

    기본 검색은 쉽다, 그냥 불러주면 웬만한 설정이 다 되어있다.

    @MatchingStrategy에 관한 글

    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

    댓글