๐Ÿ” ๋ฌธ์„œ ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ยถ

์„ธ๋ฒ• ํ•ด์„๋ก€ ์งˆ๋‹ต ๋‚ด์šฉ์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ–ˆ์œผ๋‹ˆ ์œ ์‚ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์ง€์›ํ•˜๋Š” ํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ•ด๋ด…์‹œ๋‹ค. ์žฅ๊ณ  ๋ชจ๋ธ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฌธ์„œ๊ฐ€ ์ €์žฅ๋˜์–ด์žˆ์–ด ์†์‰ฝ๊ฒŒ ์œ ์‚ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์ง€์›ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

../../_images/page1.png

๋ชจ๋ธ์— page_content_obj ์บ์‹œ ์†์„ฑ ์ถ”๊ฐ€ยถ

๋ฌธ์„œ ๋ชจ๋ธ์˜ .page_content ์†์„ฑ์€ ๋ฌธ์ž์—ด ํƒ€์ž…์ธ๋ฐ, โ€œ๋ฌธ์„œ๋ฒˆํ˜ธโ€, โ€œ์ œ๋ชฉโ€, โ€œ๋ฌธ์„œIDโ€, โ€œ๋ฒ•๋ น๋ถ„๋ฅ˜โ€, โ€œ์š”์ง€โ€, โ€œํšŒ์‹ โ€, โ€œํŒŒ์ผ๋‚ด์šฉโ€, โ€œ๊ณต๊ฐœ์—ฌ๋ถ€โ€, โ€œ๋ฌธ์„œ๋ถ„๋ฅ˜โ€ ๋“ฑ์˜ ์ •๋ณด๊ฐ€ JSON ํฌ๋งท์œผ๋กœ ์ €์žฅ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ Key ์ •๋ณด์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” JSON ์—ญ์ง๋ ฌํ™”๊ฐ€ ํ•„์š”ํ•˜๊ตฌ์š”. ๋งค Key์— ์ ‘๊ทผํ•  ๋•Œ๋งˆ๋‹ค ์—ญ์ง๋ ฌํ™”๋ฅผ ํ•˜๋ฉด ์„ฑ๋Šฅ์ด ๋–จ์–ด์ง€๋ฏ€๋กœ, page_content_obj ์บ์‹œ ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ ์ธ์Šคํ„ด์Šค๋งˆ๋‹ค 1ํšŒ๋งŒ ์—ญ์ง๋ ฌํ™”๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ , ์บ์‹ฑ๋œ ๊ฐ์ฒด๋ฅผ ํ™œ์šฉํ† ๋ก ํ•ฉ๋‹ˆ๋‹ค.

chat/models.py ํŒŒ์ผ์— ๋ฎ์–ด์“ฐ๊ธฐยถ
 1import json
 2
 3from django.utils.functional import cached_property
 4from pyhub.rag.fields.sqlite import SQLiteVectorField
 5from pyhub.rag.models.sqlite import SQLiteVectorDocument
 6
 7class TaxLawDocument(SQLiteVectorDocument):
 8    embedding = SQLiteVectorField(
 9        dimensions=3072,
10        editable=False,
11        embedding_model="text-embedding-3-large",
12    )
13
14    @cached_property
15    def page_content_obj(self):
16        return json.loads(self.page_content)
chat/models.py ํŒŒ์ผ์— ๋ฎ์–ด์“ฐ๊ธฐยถ
 1import json
 2
 3from django.utils.functional import cached_property
 4from pyhub.rag.fields.postgres import PGVectorField
 5from pyhub.rag.models.postgres import PGVectorDocument
 6
 7class TaxLawDocument(PGVectorDocument):
 8    embedding = PGVectorField(
 9        dimensions=3072,
10        editable=False,
11        embedding_model="text-embedding-3-large",
12    )
13
14    @cached_property
15    def page_content_obj(self):
16        return json.loads(self.page_content)
17
18    class Meta:
19        indexes = [
20            PGVectorDocument.make_hnsw_index(
21                "chat_taxlawdoc_idx",
22                "halfvec",
23                "cosine",
24            ),
25        ]

์ด์ œ ํŒŒ์ด์ฌ ์ฝ”๋“œ ๋‹จ์—์„œ๋Š” doc.page_content_obj["์ œ๋ชฉ"] ์ฒ˜๋Ÿผ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ…œํ”Œ๋ฆฟ ๋‹จ์—์„œ๋Š” {{ doc.page_content_obj.์ œ๋ชฉ }} ์ฒ˜๋Ÿผ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ทฐ ๊ตฌํ˜„ยถ

๋ฆฌ์ŠคํŠธ ๊ตฌํ˜„์„ ์œ„ํ•ด ListView ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์€ TaxLawDocumentListView ํด๋ž˜์Šค๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. model ์†์„ฑ๋งŒ TaxLawDocument ๋ชจ๋ธ๋กœ ์ง€์ •ํ•˜๋ฉด ํ•œ ๋ฒˆ์— ์ „์ฒด ๋ฌธ์„œ๊ฐ€ ์กฐํšŒ๋˜๋‹ˆ ์กฐํšŒ ์„ฑ๋Šฅ์ด ๋–จ์–ด์ง‘๋‹ˆ๋‹ค. paginate_by ์†์„ฑ์„ ์ง€์ •ํ•˜๋ฉด ListView๋ฅผ ํ†ตํ•ด ์ฟผ๋ฆฌ์…‹ ๊ธฐ๋ฐ˜์—์„œ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ง€์›ํ•˜์ง€๋งŒ, ์œ ์‚ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ์— paginate_by ์†์„ฑ์€ ์ง€์ •ํ•˜์ง€ ์•Š๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ฒ€์ƒ‰์–ด query ์ธ์ž๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, ๋ชจ๋ธ ๋งค๋‹ˆ์ €์˜ similarity_search ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์œ ์‚ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

ListView์—์„œ๋Š” ์ฟผ๋ฆฌ์…‹์„ ํ†ตํ•ด ๋””ํดํŠธ ํ…œํ”Œ๋ฆฟ ์ด๋ฆ„์„ ์ฐพ๋Š”๋ฐ์š”.

django-pyhub-rag ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ sqlite-vec ๋ฐฑ์—”๋“œ์˜ similarity_search ๋ฉ”์„œ๋“œ๋Š” ์ฟผ๋ฆฌ์…‹์ด ์•„๋‹Œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , get_queryset ๋ฉ”์„œ๋“œ์—์„œ๋„ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ListView ํด๋ž˜์Šค์—์„œ ๋””ํดํŠธ ํ…œํ”Œ๋ฆฟ ์ด๋ฆ„์„ ๊ฒฐ์ •ํ•  ์ˆ˜ ์—†๊ธฐ์—, template_name ์†์„ฑ์„ ์ง์ ‘ ์ง€์ •ํ•ด์ค๋‹ˆ๋‹ค.

chat/views.py ํŒŒ์ผ์— ๋ฎ์–ด์“ฐ๊ธฐยถ
 1from django.views.generic import ListView
 2from .models import TaxLawDocument
 3
 4# ํ…œํ”Œ๋ฆฟ์—์„œ์˜ URL Reverse ์ฐธ์กฐ๋ฅผ ์œ„ํ•ด ๋นˆ View ํ•จ์ˆ˜ ์ •์˜
 5def room_list(request): pass
 6def room_new(request): pass
 7def room_detail(request, pk): pass
 8
 9# ๋ฌธ์„œ ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€
10class TaxLawDocumentListView(ListView):
11    model = TaxLawDocument
12    # sqlite์˜ similarity_search ๋ฉ”์„œ๋“œ๊ฐ€ ์ฟผ๋ฆฌ์…‹์ด ์•„๋‹Œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์—
13    # ListView์—์„œ ํ…œํ”Œ๋ฆฟ ์ด๋ฆ„์„ ์ฐพ์ง€ ๋ชปํ•˜๊ธฐ์— ์ง์ ‘ ์ง€์ •ํ•ด์ค๋‹ˆ๋‹ค.
14    template_name = "chat/taxlawdocument_list.html"
15
16    def get_queryset(self):
17        qs = super().get_queryset()
18
19        query = self.request.GET.get("query", "").strip()
20        if query:
21            qs = qs.similarity_search(query)  # noqa: list ํƒ€์ž…
22        else:
23            # ๊ฒ€์ƒ‰์–ด๊ฐ€ ์—†๋‹ค๋ฉด ๋นˆ ์ฟผ๋ฆฌ์…‹์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
24            qs = qs.none()
25
26        return qs

URL ๋งคํ•‘๋„ ์ถ”๊ฐ€ํ•ด์ฃผ์‹œ๊ตฌ์š”.

chat/urls.py ํŒŒ์ผ์— ๋ฎ์–ด์“ฐ๊ธฐยถ
from django.urls import path
from . import views

app_name = "chat"

urlpatterns = [
    path("", views.room_list, name="room_list"),
    path("new/", views.room_new, name="room_new"),
    path("<int:pk>/", views.room_detail, name="room_detail"),
    path("docs/law/tax/", views.TaxLawDocumentListView.as_view()),
]

ํ…œํ”Œ๋ฆฟ ๊ตฌํ˜„ยถ

์•„๋ž˜ ๋‚ด์šฉ์œผ๋กœ chat/templates/chat/base.html ๊ฒฝ๋กœ์— ๋ถ€๋ชจ ํ…œํ”Œ๋ฆฟ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๋ฌธ์„œ ๋ชฉ๋ก ํŽ˜์ด์ง€ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์ดํ›„ ์ฑ„ํŒ… ํŽ˜์ด์ง€์—์„œ๋„ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  • tailwind css : ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ธฐ๋ฐ˜ CSS ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ๋น ๋ฅธ UI ๊ตฌ์„ฑ์„ ์œ„ํ•ด ์‚ฌ์šฉ

  • htmx : ์„œ๋ฒ„์™€์˜ ๋น„๋™๊ธฐ ํ†ต์‹ ์„ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—†์ด HTML ์†์„ฑ์œผ๋กœ ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ

  • alpine.js : ์ƒํƒฏ๊ฐ’์— ๋”ฐ๋ฅธ UI ๋™์ž‘์„ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์—†์ด ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ

chat/templates/chat/taxlawdocument_list.html ๊ฒฝ๋กœ์— ์œ„ ๋ถ€๋ชจ ํ…œํ”Œ๋ฆฟ์„ ์ƒ์†๋ฐ›์€ ํ…œํ”Œ๋ฆฟ์„ ์•„๋ž˜์™€ ๊ฐ™์ด ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

  • <form> ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ํผ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

    • <select> ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ ๋Œ€์ƒ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

    • <input> ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

    • <button> ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • {% if request.GET.query and not object_list %} ํ…œํ”Œ๋ฆฟ ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰๊ฒฐ๊ณผ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ๊ฐ€ ์—†์Œ์„ ์•Œ๋ฆฌ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.

  • {% for doc in object_list %} ํ…œํ”Œ๋ฆฟ ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜๋ณตํ•˜์—ฌ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.

์›น๋ธŒ๋ผ์šฐ์ €๋ฅผ ์—ด๊ณ  http://localhost:8000/chat/docs/law/tax/ ํŽ˜์ด์ง€์— ์ ‘์†ํ•ด์ฃผ์„ธ์š”. ์•„๋ž˜์™€ ๊ฐ™์ด ์œ ์‚ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.. ์•„๋ž˜๋Š” โ€œ์žฌํ™” ์ˆ˜์ถœํ•˜๋Š” ๊ฒฝ์šฐ ์˜์„ธ์œจ ์ฒจ๋ถ€ ์„œ๋ฅ˜๋กœ ์ˆ˜์ถœ์‹ค์ ๋ช…์„ธ์„œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•โ€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

../../_images/page2.png

๋ฌธ์„œ ์ œ๋ชฉ ํด๋ฆญํ•˜์—ฌ, ๋‚ด์šฉ ์ ‘๊ณ  ํŽด๊ธฐยถ

.similarity_search ๋ฉ”์„œ๋“œ๋Š” ๋””ํดํŠธ๋กœ ์ตœ๋Œ€ 4๊ฐœ์˜ ๋ฌธ์„œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ, ๊ฐ ๋ฌธ์„œ์˜ ๋‚ด์šฉ์ด ๋„ˆ๋ฌด ๊ธธ์–ด์„œ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ณด๊ธฐ ์–ด๋ ค์šด๋ฐ์š”. ๋ฌธ์„œ ์ œ๋ชฉ์„ ํด๋ฆญํ•˜๋ฉด ๋‚ด์šฉ์„ ์ ‘๊ณ  ํŽด๊ธฐ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ฒฝ๋Ÿ‰ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ alpine.js ์•ฝ 45KB ์šฉ๋Ÿ‰ ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฌธ์„œ ์ œ๋ชฉ์„ ํด๋ฆญํ•˜๋ฉด ๋‚ด์šฉ์„ ์ ‘๊ณ  ํŽด๊ธฐ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ…œํ”Œ๋ฆฟ์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋ถ€๋ชจ ํ…œํ”Œ๋ฆฟ์—์„œ ์ด๋ฏธ ํฌํ•จ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

  • ๊ฐ ๋ฌธ์„œ๋งˆ๋‹ค x-data="{ opened: false }" ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ๋ฌธ์„œ๋งˆ๋‹ค ๋ณ„๋„์˜ ์ƒํƒฏ๊ฐ’์„ ๊ฐ€์ง€๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

  • ๋ฌธ์„œ ๋‚ด์šฉ ์š”์†Œ๋Š” x-show="opened" ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ, opened ์ƒํƒฏ๊ฐ’์ด ์ฐธ์ผ ๋•Œ์—๋งŒ ๋ณด์ด๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

  • ๋ฌธ์„œ ์ œ๋ชฉ ์š”์†Œ๋Š” x-on:click="opened = ! opened" ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ํด๋ฆญํ•˜๋ฉด opened ์ƒํƒฏ๊ฐ’์„ ํ† ๊ธ€ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

../../_images/page3.gif