반응형

 

 

간혹 우리는 DB를 작성하고 DB에서 전체 검색을 하고 하고 싶을 때가 있습니다.

그런데 column들이 나눠져 있다보니 이를 합쳐서 검색하기도 쉽지 않고, 또 연관관계까지 고려해서 작성하기도 쉽지 않습니다.

Postgres 에서는 ts_vector로 데이터를 vector로 합치고 검색하기 쉽게 되어있습니다.

 

supabase에서도 이를 이용해서 검색을 용이하게 하기 위한 인터페이스를 제공  하고 있죠.

 

 

https://supabase.com/docs/guides/database/full-text-search?queryGroups=language&language=js#search-multiple-columns

 

(supabse 의 textSearch는 postgres의 ts_query 를 기반으로 만들어져 있다.)

 

 

그러나 문제는 client api로 단어 검색을 하려다 보면, 완성된 단어는 잘 검색이 되는데, 중간 단어는 검색이 잘 안됩니다.

 

완성되지 않은 단어의 경우에는 (Partial search 참조) 따로 RPC 함수를 만들어 사용하라고 가이드 되어있습니다.

Partial search#
Partial search is particularly useful when you want to find matches on substrings within your data.
Implementing partial search#
You can use the :* syntax with to_tsquery(). Here's an example that searches for any book titles beginning with "Lit":

 

DB함수를 만들고 RPC로 호출 하는 것도 나쁘진 않습니다.

 

db 함수를 만들고.

select title from books where to_tsvector(title) @@ to_tsquery('Lit:*');

 

client에서 아래와 같이 사용한다.

const { data, error } = await supabase.rpc('search_books_by_title_prefix', { prefix: 'Lit' })

 

 

그러나 불편한 점도 있고,

아래 처럼 하나의 함수에서 다양한 쿼리 옵션들과 합쳐서 사용하기 힘들죠.

 

 async getPaginatedOpt({columns, keyword, offset, range=10, landscapeOnly, orderBy, orderByDesc}:{columns?: string, keyword?: string, offset?: number, range?: number, landscapeOnly?: boolean, orderBy?: string, orderByDesc?: boolean}): Promise<{ v: Partial<MyItem>[], total: number }> {

    let query = supabase.from("myitems_table").select(columns||'*', { count: 'exact' });

    if(landscapeOnly){
      query = query.eq('landscaping', landscapeOnly);
    }

    if(keyword){
      // query = query.or(`s_name.ilike.%${keyword}%,kr_name.ilike.%${keyword}%,jp_name.ilike.%${keyword}%,eng_name.ilike.%${keyword}%,nk_name.ilike.%${keyword}%`);
      query = query.textSearch('text_search',`${keyword}`,{type:'plain', config:'simple',});
    }

    if(offset!==undefined && range!==undefined){
      query = query.range(offset, offset + (range<1?1:range) - 1);
    }

    if(orderBy){
      query = query.order(orderBy, { ascending: orderByDesc||false });
    }

    const { data, count, error } = await query;
    
    if (error) {
      console.error('getPaginated error:', error);
      return { v: [], total: 0 };
    }

    return { v: data as Partial<MyItem>[] ?? [], total: count ?? 0 };
  },

 

 

우연찮게 textSearch 옵션에 'raw' 를 찾았습니다.

공식적으로는 plain, phrase, websearch만 명시되어있습니다만, "PostgREST의 FTS 연산자와 동일하게 작동" 한다고 되어있습니다.

FTS에는 raw라는 query가 있고, 이를 활용하면 접두사를 사용할 수 있습니다. " 오징:*" 와 같이 prefix 형태를 사용할 수 있죠.

 

// @ts-expect-error
query = query.textSearch('text_search',`${keyword}:*`,{type:'raw', config:'simple',});

 

물론 공식 API에는 제공되는것이 아니라서 typscript lint에서는 에러로 보입니다. ㅠ_ㅠ

 

공식 문서에서는 plain, phrase, websearch 3가지만 제공 됩니다. 그리고 이것은 완성된 단어들의 조합만 검색 가능합니다.

 

때로는 위와 같이 완성되지 않은 단어 검색도 필요하게 됩니다.

 

이때는 위와 같이 raw + keyword:* 가 되도록 해서 사용해보는 것도 좋을것 같네요.

 

이 방식의 단점
여전히 완벽하지 않다.

 

이렇게 구현해서 서비스에 적용하였으나, 검색이 완벽하지 않았습니다.
예를 들어,
"펨브록웰시코기" 라는 개의 종류 이름 있다고 합시다.
이 경우 "펨브록:*" 이렇게 하면 검색이 되지만, "웰시코기:*" 이렇게는 검색이 안됩니다.
즉, 단어의 맨 앞 글자부터 나오는 것만 검색이 된다는 것이죠.

 

 

DB 내의 검색 서비스를 만드는것은 다양한 것들을 고려해야하네요.~

 

 

 

#해피 코딩

 

 

 

 

+ Recent posts