Skip to content

Commit 463c960

Browse files
committed
fix 意味的検索の高速化と精度向上
1 parent c60e7b0 commit 463c960

10 files changed

Lines changed: 45 additions & 115 deletions

File tree

backend/infrastructure/database/diary_embeddings.go

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"database/sql"
66
"fmt"
7-
"strconv"
87
"strings"
98
"time"
109

@@ -52,19 +51,17 @@ type DiaryEmbeddingStatus struct {
5251
ChunkModelVersion string
5352
CreatedAt time.Time
5453
UpdatedAt time.Time
55-
// 最初のチャンクのベクトル値(デバッグ用)
56-
EmbeddingValues []float32
5754
// チャンク総数
5855
ChunkCount int
5956
// 各チャンクの概要(chunk_index順)
6057
ChunkSummaries []string
6158
}
6259

6360
// GetDiaryEmbeddingStatus は指定された日記のRAGインデックス状態を返す
64-
// 全チャンクのsummaryとchunk_index=0のベクトル値を返す
61+
// 全チャンクのsummaryを返す(ベクトル値は返さない)
6562
func GetDiaryEmbeddingStatus(ctx context.Context, db DB, diaryID, userID uuid.UUID) (*DiaryEmbeddingStatus, error) {
6663
query := `
67-
SELECT model_version, chunk_model_version, created_at, updated_at, embedding::text, chunk_summary
64+
SELECT model_version, chunk_model_version, created_at, updated_at, chunk_summary
6865
FROM diary_embeddings
6966
WHERE diary_id = $1 AND user_id = $2
7067
ORDER BY chunk_index ASC
@@ -81,24 +78,22 @@ func GetDiaryEmbeddingStatus(ctx context.Context, db DB, diaryID, userID uuid.UU
8178
var (
8279
modelVersion, chunkModelVersion string
8380
createdAt, updatedAt time.Time
84-
embeddingValues []float32
8581
chunkSummaries []string
8682
)
8783
first := true
8884

8985
for rows.Next() {
90-
var embStr, chunkSummary string
86+
var chunkSummary string
9187
var mv, cmv string
9288
var cat, uat time.Time
93-
if err := rows.Scan(&mv, &cmv, &cat, &uat, &embStr, &chunkSummary); err != nil {
89+
if err := rows.Scan(&mv, &cmv, &cat, &uat, &chunkSummary); err != nil {
9490
return nil, fmt.Errorf("failed to scan diary embedding status: %w", err)
9591
}
9692
if first {
9793
modelVersion = mv
9894
chunkModelVersion = cmv
9995
createdAt = cat
10096
updatedAt = uat
101-
embeddingValues = parseEmbeddingString(embStr)
10297
first = false
10398
}
10499
chunkSummaries = append(chunkSummaries, chunkSummary)
@@ -118,32 +113,11 @@ func GetDiaryEmbeddingStatus(ctx context.Context, db DB, diaryID, userID uuid.UU
118113
ChunkModelVersion: chunkModelVersion,
119114
CreatedAt: createdAt,
120115
UpdatedAt: updatedAt,
121-
EmbeddingValues: embeddingValues,
122116
ChunkCount: len(chunkSummaries),
123117
ChunkSummaries: chunkSummaries,
124118
}, nil
125119
}
126120

127-
// parseEmbeddingString はpgvectorの文字列表現をfloat32スライスに変換する
128-
// pgvectorは "[v1,v2,...,vn]" 形式で返す(科学表記も含む)
129-
func parseEmbeddingString(s string) []float32 {
130-
s = strings.TrimPrefix(s, "[")
131-
s = strings.TrimSuffix(s, "]")
132-
if s == "" {
133-
return nil
134-
}
135-
parts := strings.Split(s, ",")
136-
values := make([]float32, 0, len(parts))
137-
for _, p := range parts {
138-
p = strings.TrimSpace(p)
139-
f, err := strconv.ParseFloat(p, 32)
140-
if err == nil {
141-
values = append(values, float32(f))
142-
}
143-
}
144-
return values
145-
}
146-
147121
// embeddingToSQL はfloat32スライスをpgvector形式の文字列に変換する
148122
func embeddingToSQL(v []float32) string {
149123
parts := make([]string, len(v))

backend/infrastructure/grpc/diary.pb.go

Lines changed: 13 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/infrastructure/llm/gemini.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -312,17 +312,19 @@ func (g *GeminiClient) GenerateEmbedding(ctx context.Context, text string, isDoc
312312
// 各チャンクは意味的に自己完結したテキストで、ベクトル検索の精度向上に使用する
313313
// LLMの呼び出しに失敗した場合はエラーを返す(呼び出し元でフォールバック処理を行う)
314314
func (g *GeminiClient) SplitDiaryIntoChunks(ctx context.Context, content string) ([]DiaryChunkData, error) {
315-
prompt := fmt.Sprintf(`以下の日記を、話題・場面の切れ目で分割してください。
315+
prompt := fmt.Sprintf(`以下の日記を、話題・場面ごとのチャンクに分割してください。
316+
チャンク数はできるだけ少なくすることが重要です。
316317
317318
【分割ルール(必ず守ること)】
318-
1. 段落(空行で区切られた段落)を基本単位とする
319-
2. 同じ段落が複数の話題を含む場合のみ、文単位でさらに分割する
320-
3. 連続する段落が同じ話題・場面を扱っている場合は、1つのチャンクにまとめる
321-
4. 日記全体が1つの話題・場面のみの場合は、必ず1チャンクにまとめる
322-
5. チャンク数の目安: 段落数と同数か、それより少ない数になるはず
323-
6. contentは元の日記の文章をそのまま使う(要約・改変・省略禁止)
324-
7. チャンク間で内容が重複しないようにする
325-
8. summaryはそのチャンクの内容を1文で簡潔にまとめた日本語(句点「。」で終わる)
319+
1. 同じ話題・場面・出来事に関する文章は、段落をまたいでも必ず1つのチャンクにまとめる
320+
2. 例:「朝の出来事」が複数の段落にわたって書かれていれば、すべて1チャンクにする
321+
3. 明らかに異なる話題・場面に切り替わった場合のみ、新しいチャンクを作る
322+
4. 日記全体が1〜2の話題のみの場合は、1チャンクにまとめる
323+
5. チャンク数の上限目安: 明確に異なる話題の数(通常1〜3個)
324+
6. 細かく分割しすぎない: 「朝食を食べた」「仕事をした」「夜に映画を見た」は3チャンクではなく、関連性があれば1〜2チャンクにまとめる
325+
7. contentは元の日記の文章をそのまま使う(要約・改変・省略禁止)
326+
8. チャンク間で内容が重複しないようにする
327+
9. summaryはそのチャンクの内容を1文で簡潔にまとめた日本語(句点「。」で終わる)
326328
327329
日記:
328330
%s`, content)

backend/service/diary/service.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,19 +1110,31 @@ func (s *DiaryEntry) SearchDiaryEntriesSemantic(
11101110
return nil, status.Errorf(codes.Internal, "Failed to set hnsw.ef_search: %v", err)
11111111
}
11121112

1113-
// pgvectorでコサイン類似度ANN検索
1113+
// ベクトル検索とキーワード検索を並列実行してレイテンシを削減
1114+
// キーワード検索はef_searchを必要としないため別コネクション(s.DB)で並列化できる
1115+
type keywordSearchResult struct {
1116+
diaries []*database.Diary
1117+
err error
1118+
}
1119+
kwResultCh := make(chan keywordSearchResult, 1)
1120+
go func() {
1121+
ds, err := database.DiariesByUserIDAndContent(ctx, s.DB, userID.String(), req.Query)
1122+
kwResultCh <- keywordSearchResult{diaries: ds, err: err}
1123+
}()
1124+
1125+
// pgvectorでコサイン類似度ANN検索(txのef_search設定を使用)
11141126
searchResults, err := database.SearchDiaryEntriesByEmbedding(ctx, tx, userID, queryEmbedding, limit, semanticSimilarityThreshold)
11151127
if err != nil {
11161128
return nil, status.Errorf(codes.Internal, "Failed to search diary entries: %v", err)
11171129
}
11181130

1119-
// ハイブリッド検索: キーワード LIKE 検索で補完(ベクトル検索が拾えない固有名詞・専門語をカバー)
1131+
// ハイブリッド検索: キーワード検索結果で補完(ベクトル検索が拾えない固有名詞・専門語をカバー)
1132+
kwResult := <-kwResultCh
11201133
vectorIDs := make(map[uuid.UUID]bool, len(searchResults))
11211134
for _, sr := range searchResults {
11221135
vectorIDs[sr.DiaryID] = true
11231136
}
1124-
keywordResults, _ := database.DiariesByUserIDAndContent(ctx, tx, userID.String(), req.Query)
1125-
for _, d := range keywordResults {
1137+
for _, d := range kwResult.diaries {
11261138
if !vectorIDs[d.ID] {
11271139
searchResults = append(searchResults, &database.DiaryEmbeddingSearchResult{
11281140
DiaryID: d.ID,
@@ -1302,10 +1314,6 @@ func (s *DiaryEntry) GetDiaryEmbeddingStatus(
13021314
resp.ChunkModelVersion = embeddingStatus.ChunkModelVersion
13031315
resp.CreatedAt = embeddingStatus.CreatedAt.Unix()
13041316
resp.UpdatedAt = embeddingStatus.UpdatedAt.Unix()
1305-
// レスポンスサイズ削減のため先頭10件のみ返す(フロントエンドはプレビュー表示のみ)
1306-
resp.EmbeddingDimensions = int32(len(embeddingStatus.EmbeddingValues))
1307-
previewLen := min(10, len(embeddingStatus.EmbeddingValues))
1308-
resp.EmbeddingValues = embeddingStatus.EmbeddingValues[:previewLen]
13091317
resp.ChunkCount = int32(embeddingStatus.ChunkCount)
13101318
resp.ChunkSummaries = embeddingStatus.ChunkSummaries
13111319
}

0 commit comments

Comments
 (0)