11. Document 추상화 모델¶
변경 파일을 한 번에 덮어쓰기 하실려면, pyhub-git-commit-apply 유틸리티 설치하신 후에, rag-02 폴더 상위 경로에서 아래 명령어 실행
uv run pyhub-git-commit-apply https://github.com/pyhub-kr/django-llm-chat-proj/commit/ba2dc25d4555f89dab979a800bbc750ba655d1d3
11.1. 여러 Document 모델을 둘려면?¶
PaikdabangMenuDocument
모델은 빽다방 메뉴 데이터를 저장하고 임베딩하는 모델입니다.
그런데, page_content
, metadata
, embedding
필드 등
여러 Document 모델에서 공통적으로 사용되는 필드로만 구성되어있습니다.
빽다방 관련된 부분은 모델명 밖에 없죠.
추가로 StarbucksMenuDocument
모델을 만들려면 PaikdabangMenuDocument
모델 코드를 복사&붙여넣기 하기보다, 장고의 추상화 모델 클래스를 상속받아 구현하시면
코드 중복없이 간결하게 여러 Document
모델을 정의하실 수 있게 됩니다.
기존 PaikdabangMenuDocument
클래스의 이름은 Document
로 변경하고,
PaikdabangMenuDocumentQuerySet
클래스의 이름은 DocumentQuerySet
로 변경합니다.
그리고 Document
클래스는 Meta.abstract = True
설정으로 추상화 클래스로 선언하고
Meta.indexes
속성은 제거합니다.
인덱스 정책은 모델마다 달라질 수 있고, 각 인덱스 name
은
유일한 이름으로 지정되어야 하기에 추상화 클래스에서는 지정할 수 없고,
상속받은 모델에서 인덱스를 직접 정의하도록 하겠습니다.
chat/models.py
¶ 1# class PaikdabangMenuDocumentQuerySet(models.QuerySet):
2class DocumentQuerySet(models.QuerySet):
3 ...
4
5 async def search(self, question: str, k: int = 4) -> List["Document"]:
6 ...
7
8# class PaikdabangMenuDocument(LifecycleModelMixin, models.Model):
9class Document(LifecycleModelMixin, models.Model):
10 ...
11
12 class Meta:
13 abstract = True
14
15 # 인덱스는 추상화 클래스가 아닌 실제 모델에서 정의해야만 합니다.
16 # indexes = [
17 # HnswIndex(
18 # name="paikdabang_menu_doc_idx",
19 # fields=["embedding"],
20 # m=16,
21 # ef_construction=64,
22 # opclasses=["vector_cosine_ops"],
23 # ),
24 # ]
이제 PaikdabangMenuDocument
모델과 StarbucksMenuDocument
모델은 Document
모델 상속 만으로 아래와 같이 손쉽게 정의할 수 있게 됩니다.
PaikdabangMenuDocument
모델의 인덱스를 복사해서 인덱스명만 변경해주겠습니다.
chat/models.py
¶ 1class PaikdabangMenuDocument(Document):
2 class Meta:
3 indexes = [
4 HnswIndex(
5 name="paikdabang_menu_doc_idx",
6 fields=["embedding"],
7 m=16,
8 ef_construction=64,
9 opclasses=["vector_cosine_ops"],
10 ),
11 ]
12
13class StarbucksMenuDocument(Document):
14 class Meta:
15 indexes = [
16 HnswIndex(
17 name="starbucks_menu_doc_idx",
18 fields=["embedding"],
19 m=16,
20 ef_construction=64,
21 opclasses=["vector_cosine_ops"],
22 ),
23 ]
마이그레이션을 해주시면
uv run python manage.py makemigrations chat
uv run python manage.py migrate chat
StarbucksMenuDocument
모델에 대한 테이블도 생성되고,
DocumentQuerySet
클래스에서 정의한 search
메서드를 통해
질문과 유사한 문서 검색을 할 수 있게 됩니다.
11.2. 모델 인덱스에 맞춰 검색하기¶
인덱스를 정의할 때 인덱스 생성에 사용될 벡터 연산 클래스를 지정하고, 데이터베이스 조회 쿼리도 맞춰 작성해야만 합니다.
pgvector
확장에서 지원하는 벡터 연산 목록은 장고 모델에 pgvector 적용 문서에 정리되어있습니다.
chat/models.py
¶ 1class PaikdabangMenuDocument(Document):
2 class Meta:
3 indexes = [
4 HnswIndex(
5 name="paikdabang_menu_doc_idx",
6 fields=["embedding"],
7 m=16,
8 ef_construction=64,
9 opclasses=["vector_cosine_ops"],
10 ),
11 ]
타입에 따라 다른 코사인 거리 연산 클래스
같은 코사인 거리 연산이더라도 필드 타입에 따라 인덱스 생성에 사용해야할 연산 클래스가 다릅니다.
VectorField
필드에 대한 코사인 거리 :vector_cosine_ops
HalfVectorField
필드에 대한 코사인 거리 :halfvec_cosine_ops
검색 시에는 동일하게 CosineDistance
데이터베이스 함수를 사용합니다.
인덱스 정의는 Document
모델 클래스에서 이뤄지고, 검색 쿼리는 DocumentQuerySet.search
메서드에서 이뤄집니다.
search
메서드를 개선하여 Document
모델의 인덱스 선언에 맞춰 쿼리를 작성할 수 있도록 하겠습니다.
코사인 거리 연산 클래스는 vector_cosine_ops
이고 DocumentQuerySet
에서 search
메서드에서는
인덱스를 활용할 수 있도록 CosineDistance
데이터베이스 함수를 통해 거리를 계산합니다.
chat/models.py
¶ 1from django.core.exceptions import ImproperlyConfigured
2from django.db.models import Index
3from pgvector.django import CosineDistance, L2Distance
4
5class DocumentQuerySet(models.QuerySet):
6 # ...
7
8 async def search(self, question: str, k: int = 4) -> List["Document"]:
9 question_embedding: List[float] = await self.model.aembed(question)
10
11 qs = None
12 index: Index
13 for index in self.model._meta.indexes:
14 if "embedding" in index.fields:
15 # vector_cosine_ops, halfvec_cosine_ops, etc.
16 if any("_cosine_ops" in cls for cls in index.opclasses):
17 qs = (qs or self).annotate(
18 distance=CosineDistance("embedding", question_embedding)
19 )
20 qs = qs.order_by("distance")
21 # vector_l2_ops, halfvec_l2_ops, etc.
22 elif any("_l2_ops" in cls for cls in index.opclasses):
23 qs = (qs or self).annotate(
24 distance=L2Distance("embedding", question_embedding)
25 )
26 qs = qs.order_by("distance")
27 else:
28 raise NotImplementedError(f"{index.opclasses}에 대한 검색 구현이 필요합니다.")
29
30 if qs is None:
31 raise ImproperlyConfigured(f"{self.model.__name__} 모델에 embedding 필드에 대한 인덱스를 추가해주세요.")
32
33 return await sync_to_async(list)(qs[:k])
팁
pgvector
확장을 통해 여러 벡터 연산 클래스가 지원되지만, 본 튜토리얼에서는
코사인 거리와 L2 거리 연산 클래스만 구현했습니다.
11.3. make_vector_store 명령 개선¶
기존의 make_vector_store
명령은 PaikdabangMenuDocument
모델에 대한 벡터 저장소를 생성하는 명령이었습니다.
이제 Document
모델 상속 만으로 손쉽게 새로운 문서 모델을 만들 수 있으니,
make_vector_store
명령도 다양한 문서 모델을 지원하도록 개선해보겠습니다.
model
인자로 저장할 Document 모델 경로를앱이름.모델명
포맷으로 지정합니다.get_model_class
메서드는 모델 경로를 받아 모델 클래스를 임포트하고, 모델 클래스의 유효성을 검증한 뒤에, 모델 클래스를 반환합니다.handle
메서드에서는model
문자열 인자로 모델 클래스를 조회하고, 이를 활용합니다.
chat/management/commands/make_vector_store.py
¶ 1import sys
2from pathlib import Path
3from typing import Type
4
5from django.core.management import BaseCommand
6from django.db.models import Model
7from django.utils.module_loading import import_string
8from tqdm import tqdm
9
10from chat import rag
11from chat.models import Document
12
13
14class Command(BaseCommand):
15 def add_arguments(self, parser):
16 parser.add_argument(
17 "model",
18 type=str,
19 help="저장할 Document 모델 경로 (예: 'chat.PaikdabangMenuDocument')",
20 )
21 parser.add_argument(
22 "txt_file_path",
23 type=str,
24 help="VectorStore로 저장할 원본 텍스트 파일 경로",
25 )
26
27 def print_error(self, msg: str) -> None:
28 self.stdout.write(self.style.ERROR(msg))
29 sys.exit(1)
30
31 def get_model_class(self, model_path: str) -> Type[Model]:
32 try:
33 module_name, class_name = model_path.rsplit(".", 1)
34 dotted_path = ".".join((module_name, "models", class_name))
35 ModelClass: Type[Model] = import_string(dotted_path)
36 except ImportError as e:
37 self.print_error(f"{model_path} 경로의 모델을 임포트할 수 없습니다. ({e})")
38
39 if not issubclass(ModelClass, Document):
40 self.print_error("Document 모델을 상속받은 모델이 아닙니다.")
41 elif ModelClass._meta.abstract:
42 self.print_error("추상화 모델은 사용할 수 없습니다.")
43
44 return ModelClass
45
46 def handle(self, *args, **options):
47 model_name = options["model"]
48 txt_file_path = Path(options["txt_file_path"])
49
50 ModelClass = self.get_model_class(model_name)
51
52 doc_list = rag.load(txt_file_path)
53 print(f"loaded {len(doc_list)} documents")
54 doc_list = rag.split(doc_list)
55 print(f"split into {len(doc_list)} documents")
56
57 new_doc_list = [
58 ModelClass(
59 page_content=doc.page_content,
60 metadata=doc.metadata,
61 )
62 for doc in tqdm(doc_list)
63 ]
64 ModelClass.objects.bulk_create(new_doc_list)
이제 make_vector_store
명령에서 지식 데이터 파일 경로와 함께 모델 클래스 경로를 지정하여 벡터 저장소에 지식을 저장할 수 있게 됩니다.
uv run python manage.py make_vector_store chat.PaikdabangMenuDocument ./chat/assets/빽다방.txt