brunch

You can make anything
by writing

C.S.Lewis

by Dan Oct 25. 2024

비 개발자의 검색 구현 좌충우돌

PM의 개발 도전

퇴근 후나 주말, 짬을 내어 사이드 프로젝트를 혼자 하고 있는데, AI와 함께 직접 개발을 해보고 있다.


여러 기능 중 검색을 구현해야 했는데, 사용자가 입력한 검색어에 맞춰 관련성 높은 결과를 제공하는 검색 기능을 구현하려는 과정에서 고민이 되는 지점이 생겼고, 그 과정을 공유해 본다.


사실 처음에는 쉽게 생각했는데 여러 검색어에 대한 처리, 검색 결과의 우선순위 설정, 성능 최적화 등 고민할 부분이 많았다. 또 혼자 개발하는 프로젝트라 난도가 높고 복잡한 방법은 무리가 있었고, 클라우드타입이라는 서비스에서 제한된 형태의 DB인스턴스를 띄웠기 때문에 PostgreSQL의 내장 기능을 최대한 활용하면서 사용자 경험을 개선할 수 있는 방법을 찾는 것이 핵심이었다.


검색어 처리 및 불용어 제거

사용자가 입력한 검색어가 단순한 단어 하나가 아니라 복잡한 문장일 경우, 단순히 공백으로 나누는 것만으로는 충분하지 않았다. 예를 들어, “제품을 매니징 하는 방법”과 같은 문장에서 중요한 단어들만 추출하고, 의미가 적거나 검색과 관련 없는 단어(예: “방법”, “있는”, “하는”)를 제외할 필요가 있었다.


이 문제를 해결하기 위해, Python의 Konlpy 라이브러리와 Okt 형태소 분석기를 사용했다. 이를 통해 입력된 문장에서 중요한 단어만을 추출하고, 불필요한 단어는 불용어 리스트를 이용해 제거했다.


from konlpy.tag import Okt


stopwords = ['방법', '있는', '하는', '으로', '저런', '그런']


def extract_keywords(search_query):

    okt = Okt()

    keywords = okt.nouns(search_query)

    filtered_keywords = [word for word in keywords if word not in stopwords]

    return filtered_keywords


search_query = "제품을 매니징 하는 방법"

search_terms = extract_keywords(search_query)


print(search_terms)  # ['제품', '매니징']


이 과정에서 “제품”, “매니징”과 같은 핵심 단어만 남기고, 검색에 불필요한 “방법”과 같은 단어는 제외되었다. 이렇게 추출된 단어들은 이후 PostgreSQL의 Full-Text Search에서 사용되어 더 정확한 검색 결과를 도출할 수 있었다.


[참고]
불용어(Stopwords)는 텍스트 처리에서 의미가 적거나 자주 등장하는 단어를 의미하며, 프로젝트의 특성에 맞게 커스터마이즈 할 수 있다. 한국어 불용어 리스트는 기본적으로 조사, 접속사, 의미가 약한 명사 등을 포함하며, Github의 오픈소스 프로젝트​에서도 불용어 리스트를 참고할 수 있다.


검색 결과 우선순위

사용자가 검색했을 때, 어떤 결과를 먼저 보여줄지 결정하는 것이 매우 중요했다. 검색어가 제목, 본문, 태그, 키포인트 중 어디에 있느냐에 따라 결과의 관련성이 달라지기 때문이다. 예를 들어, 제목에 검색어가 포함된 결과는 사용자에게 더 관련성이 높은 정보로 보일 수 있다. 반면, 본문에만 포함된 정보는 덜 중요할 수 있다.


그래서 제목에 검색어가 있을 때 가장 높은 가중치를 부여했고, 그다음으로 본문, 키포인트, 태그 순으로 가중치를 낮췄다. PostgreSQL의 ts_rank와 setweight 기능을 활용해, 검색 결과를 자동으로 우선순위에 따라 정렬할 수 있었다.      



SELECT *,

    ts_rank(

        setweight(to_tsvector('korean', title), 'A') ||  

        setweight(to_tsvector('korean', content), 'B') ||

        setweight(to_tsvector('korean', keypoints), 'C') ||

        setweight(to_tsvector('korean', tags), 'D'),  

        to_tsquery('korean', '프로덕트 & 매니징')

    ) AS rank

FROM articles

ORDER BY rank DESC;


이로써 사용자는 제목에 검색어가 포함된, 더 관련성 높은 결과를 먼저 볼 수 있었다. 본문이나 태그에만 포함된 경우는 그다음 순위로 나타나게 했다.


[참고]
- ts_rank는 검색 결과의 관련성을 평가하는 함수로, 텍스트 검색에서 얼마나 중요한 위치에 검색어가 있는지 계산한다.
- setweight는 특정 필드에 가중치를 부여하여, 중요한 필드(예: 제목)에 더 높은 점수를 부여할 수 있게 해 준다. 가중치는 'A'(가장 높음)부터 'D'(가장 낮음)까지 부여 가능하다.


검색어 처리 방식

사용자가 여러 개의 검색어를 입력했을 때, 이 검색어들을 어떻게 처리할지도 고민했다. "프로덕트"와 "매니징"이라는 검색어를 함께 입력하면 두 단어 모두 포함된 결과만 보여줘야 할지, 하나만 포함된 경우도 보여줄지를 결정해야 했다.


PostgreSQL의 Full-Text Search(FTS)를 활용해 이 문제를 해결했다. 여러 검색어를 받아 AND 조건으로 처리해 두 단어가 모두 포함된 경우만 결과로 반환하거나, 필요에 따라 OR 조건으로 더 유연한 결과를 제공할 수 있었다. 이렇게 검색어 처리 방식을 유연하게 조정함으로써, 사용자가 입력한 검색어에 맞춰 정확한 검색 결과를 반환할 수 있었다.      


@app.get("/search/")

async def search_articles(

    search_terms: list[str] = Query(...),

    db: SessionLocal = next(get_db())

):

    fts_query = ' & '.join(search_terms)


    query = text(f"""

        SELECT * FROM articles

        WHERE

            to_tsvector('korean', title) @@ to_tsquery('korean', :fts_query)

            OR to_tsvector('korean', content) @@ to_tsquery('korean', :fts_query)

            OR to_tsvector('korean', keypoints) @@ to_tsquery('korean', :fts_query)

            OR to_tsvector('korean', tags) @@ to_tsquery('korean', :fts_query)

        ORDER BY rank DESC;

    """)


    result = db.execute(query, {"fts_query": fts_query}).fetchall()

    return {"results": [dict(row) for row in result]}


이 방식으로 사용자는 복수의 검색어를 입력했을 때도 정확한 검색 결과를 얻을 수 있었다.


검색 성능 최적화

사용자가 여러 필드에서 검색어를 찾을 때, 성능 저하가 생길 가능성을 미리 고려했다. 단순히 LIKE를 사용하는 것보다는, 성능을 높일 수 있는 방법을 찾아보고 싶었다. PostgreSQL의 GIN 인덱스를 적용해 성능을 최적화했다. 이를 통해 데이터가 많아져도 검색 성능을 유지할 수 있었다.


CREATE INDEX idx_articles_fts ON articles USING GIN(to_tsvector('korean', title, content, keypoints, tags));


이 방식으로, 다중 필드에서 검색어를 처리하면서도 빠른 속도를 유지할 수 있었다. 검색어가 많이 포함된 데이터에서도 사용자에게 즉각적인 응답을 제공할 수 있게 되었다.


[참고]
GIN 인덱스는 PostgreSQL에서 사용하는 특수한 인덱스 방식으로, 여러 값(예: 텍스트의 여러 단어 또는 JSON 필드)을 빠르게 검색할 때 사용된다. Full-Text Search나 JSON 데이터의 빠른 검색을 지원하며, 성능 향상에 유리하다.
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari