[android]Composeのanimate関数

class MainActivity : ComponentActivity() {

    overrider fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(saveInstanceState)
        setContent {
            MaterialTheme {
                AnimatedBoxExample()
            }
        }
    }

    @Composable
    fun AnimatedBoxExample() {
        var expaned by remember {mutableStateOf(false)}

        val boxSize by animateDpAsState(
            targetValue = if(expanded) 200.dp else 100.dp,
            label ="BoxSizeAnimation"
        )

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(32.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Box(
                modifier = Modifier
                    .size(boxSize)
                    .padding(8.dp),
                contentAlignment = Alignment.Center
            ) {
                color = MaterialTheme.colorScheme.primary,
                modifier = Modifier.fillMaxSize()
            } {}
        }
        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick= {expanded = !expanded}) {
            Text(text =if (expanded) "縮小する" else "拡大する")
        }
    }
}

animateDpAsState:Dp 値を滑らかにアニメーションさせます。
targetValue:アニメーションで変化させたい目標値。
expanded を切り替えることで、Boxのサイズが 100.dp ↔ 200.dp にアニメーション付きで変化。

[LLM] トランスフォーマー構造の概要とAttentionの考え方

## 1. トランスフォーマーとは?
トランスフォーマーは2017年に登場した自然言語処理のニューラルネットワーク構造
RNNやCNNと違って系列データの処理を並列化できるのが最大の特徴
– 入力: 文章(単語やトークンに分けたもの)
– 出力: 文章の予測や分類など
– 主な構成:
1. エンコーダ: 入力を内部表現に変換
2. デコーダ: 内部表現から出力を生成
LLM(ChatGPTなど)は、基本的にデコーダ中心のトランスフォーマを使っている

## Attentionの考え方
Attentionは「文章の中でどの単語に注目すべきかを学習する仕組み」
文章: 「猫がネズミを追いかける」
「追いかける」を予測するとき、
「猫」が主語だから重要
「ネズミ」も目的語として重要
他の単語(「が」や「を」)はそれほど重要ではない
これを 数値化して重み付け するのが Attention の仕組み

## Self-Attentionの仕組み
トランスフォーマーでは 各単語が文章の他の単語を参照して特徴を作る
これを Self-Attention と呼ぶ

Query (Q):注目するための質問
Key (K):候補の特徴
Value (V):候補の情報

Query と Key の内積 → 「どれくらい注目すべきか」を示すスコア
Softmax で正規化 → 重みを 0〜1 に
Value に重みをかけて足し合わせ → 注目した情報の合成

## Multi-Head Attention
Attention は 1種類の見方だけでなく、複数の異なる視点で注目することができます。
これを Multi-Head Attention と呼ぶ

例:
1つ目のヘッド:主語と動詞の関係に注目
2つ目のヘッド:目的語と動詞の関係に注目
これにより、文脈を多面的に理解できる

なるほど~

[iOS/Swift] ネットワーク通信

swiftでネットワーク通知を学ぶ
– URLSession でAPIを叩く(例:天気アプリでAPIから天気データを取得)
– JSONデータのデコード(Codable)

対象API
https://api.open-meteo.com/v1/forecast?latitude=35.6895&longitude=139.6917&current_weather=true

データ

{
    "latitude":35.6895,
    "longitude":139.6917,
    "generationtime_ms":0.234,
    "utc_offset_seconds":0,
    "current_weather":{
        "temperature":28.3,
        "windspeed":5.2,
        "winddirection":90
    }
}

Swiftの場合だと、New Group で新規 Views, Modelsなどのディレクトリを作成する
– Views:画面用の SwiftUI View ファイルを入れる
– Models:JSON の Codable 構造体やデータモデルを入れる
models/Weather.swift

import Foundation

struct WeatherResponse: Codable {
    let current_weather: CurrentWeather
}

struct CurrentWeather: Codable {
    let temperature: Double
    let windspeed: Double
    let winddirection: Double
}

ContentView

import SwiftUI

struct ContentView: View {
    @State private var temperature: String = "__"
    @State private var windspeed: String = "__"
    
    var body: some View {
        VStack(spacing: 20) {
            Text("東京の現在天気")
                .font(.title)
            
            Text("気温: \(temperature)°C")
            Text("風速: \(windspeed)m/s")
            
            Button("天気を取得") {
                fetchWeather()
            }
        }
        .padding()
    }
    
    func fetchWeather() {
        guard let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=35.6895&longitude=139.6917&current_weather=true") else { return }
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                do {
                    let decoded = try JSONDecoder().decode(WeatherResponse.self, from: data)
                    DispatchQueue.main.async {
                                            self.temperature = String(decoded.current_weather.temperature)
                                            self.windspeed = String(decoded.current_weather.windspeed)
                                        }
                } catch {
                    print("JSONデコードエラー: \(error)")
                }
            } else if let error = error {
                print("通信エラー: \(error)")
            }
        }.resume()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

うおおおおおおおお、何だこれは すげえ

ポイント
– URLSession.shared.dataTask で非同期に API を叩く
– Codable で JSON を Swift の構造体にデコード
– DispatchQueue.main.async で UI 更新
– @State で変数をバインドして、UI に表示

[Figma] デザインを効率化・再利用する仕組みコンポーネント

### コンポーネントとは?
Figma内で再利用できる「部品テンプレート」です。
– 元(Main component)を変更すると、コピー(Instances)全てに反映される
– チームで一貫したデザインを維持できる
– 例えば「ボタン」「カード」「ヘッダー」などで使う

まずcomponentの作成

componentをフレームで利用する

テキスト内容は変更できる

### Variants
複数のデザインパターンを切り替えられるようにする
props = hover, default, … などと選択できるようにしていく

なるほど!なかなか奥が深い!

DifyにRedisを追加したい

$ sudo apt update
$ sudo apt install redis-server
$ sudo systemctl enable redis-server
$ sudo systemctl start redis-server

$ redis-cli ping

Vagrantfile

  config.vm.network "forwarded_port", guest: 6379, host: 6379 # 追加

うまくいかんな…

[android] データの非同期処理(Coroutines / Flow)

MainViewModel.kt

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {
    private val _counter = MutableStateFlow(0)
    val counter: StateFlow<Int> = _counter

    fun startCounting() {
        viewModelScope.launch {
            for (i in 1..10) {
                delay(1000) // 1秒待つ
                _counter.value = i
            }
        }
    }
}
package com.example.myapplicationcoroutine

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import android.widget.Button
import android.widget.TextView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.myapplicationcoroutine.ui.theme.MyApplicationCoroutineTheme

class MainActivity : ComponentActivity() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val counterTextView = findViewById<TextView>(R.id.counterTextView)
        val startButton = findViewById<Button>(R.id.startButton)

        // ボタンを押すとカウントスタート
        startButton.setOnClickListener {
            viewModel.startCounting()
        }

        // Flowを監視してUI更新
        lifecycleScope.launch {
            viewModel.counter.collectLatest { value ->
                counterTextView.text = "カウント: $value"
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="16dp">

    <TextView
        android:id="@+id/counterTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="カウント: 0"
        android:textSize="24sp"
        android:layout_marginBottom="24dp"/>

    <Button
        android:id="@+id/startButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="カウント開始"/>
</LinearLayout>

[Swift] UsersDefaults

UsersDefaultsとはiOS や macOS アプリで ちょっとしたデータを簡単に保存できる仕組み です。

使い所
ユーザー名やメールアドレス
設定(ダークモードON/OFF、通知ON/OFFなど)
チュートリアルを見たかどうか
ちょっとしたリスト(お気に入りのIDなど)

struct ContentView: View {
    @State private var userMessage: String = ""
    @State private var aiReply: String = ""
    
    @State private var items: [String] = UserDefaults.standard.stringArray(forKey: "items") ?? ["りんご", "バナナ", "みかん"]
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("AI Chat Demo")
                    .font(.title)
                
                TextField("メッセージを入力", text: $userMessage)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                
                Button("送信") {
                    sendMessage()
                }
                .padding()
                
                Text("AIの返答: \(aiReply)")
                    .padding()
                
                Divider()
                
                List {
                    ForEach(items, id:\.self) { item in
                        Text(item)
                    }
                    .onDelete(perform: deleteItem)
                    .onMove(perform: moveItem)
                }
                .frame(height: 200)
                
                Button("フルーツを追加") {
                    addItem()
                }
                .padding()
                
                Spacer()
                
                NavigationLink(destination: ImageViewPage()) {
                    Text("画像ページへ移動")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(8)
                }
                NavigationLink(destination: VideoViewPage()) {
                    Text("動画ページへ移動")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.green)
                        .cornerRadius(8)
                }
//                NavigationLink(destination: CounterPage()) {
//                    Text("カウンターページへ")
//                        .foregroundColor(.white)
//                        .padding()
//                        .background(Color.green)
//                        .cornerRadius(8)
//                }
            }
            .padding()
            .navigationTitle("チャット")
        }
    }
    
    func saveItems() {
        UserDefaults.standard.set(items, forKey: "items")
    }
    
    func addItem() {
        items.append("新しいフルーツ\(items.count + 1)")
        saveItems()
    }
    
    func deleteItem(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
        saveItems()
    }
    
    func moveItem(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
        saveItems()
    }

[Figma]タイポグラフィ

左ツールバーの Text (T) を選択
画面にクリックして文字入力(例:「メールアドレス」)
ステップ2:文字の属性を変更
右パネルの Text セクションで変更可能:
Font family:フォントの種類
Weight:太さ(Regular, Bold, etc.)
Size:フォントサイズ
Line height:行の高さ
Letter spacing:文字間隔
Text decoration:下線・打ち消し線など

毎日5~10分でも変わりそうやな

Google ColaboでQdrant用のベクトルデータを生成してQdrantを試す

# 前準備
### jsonデータ生成
ライブドアニュースコーパスをDL

ダウンロード


ダウンロード(通常テキスト):ldcc-20140209.tar.gz

### コーパスの取り込み

import json
import datetime
from typing import List, Dict
from pathlib import Path
import random

CORPUS_DIR = './livedoor-corpus'  # ライブドアコーパスをここにおく
QDRANT_JSON = 'livedoor.json'
SAMPLE_TEXT_LEN: int = 500  # ドキュメントを500文字でトランケート


def read_document(path: Path) -> Dict[str, str]:
    """1ドキュメントの処理"""
    with open(path, 'r') as f:
        lines: List[any] = f.readlines(SAMPLE_TEXT_LEN)
        lines = list(map(lambda x: x.rstrip(), lines))

        d = datetime.datetime.strptime(lines[1], "%Y-%m-%dT%H:%M:%S%z")
        created_at = int(round(d.timestamp()))  # 数値(UNIXエポックタイプ)に変換

        return {
            "url": lines[0],
            "publisher": path.parts[1],  # ['livedoor-corpus', 'it-life-hack', 'it-life-hack-12345.txt']
            "created_at": created_at,
            "body": ' '.join(lines[2:])  # 初めの2行をスキップし、各行をスペースで連結し、1行にする。
        }


def load_dataset_from_livedoor_files() -> (List[List[float]], List[str]):
    # NB. exclude LICENSE.txt, README.txt, CHANGES.txt
    corpus: List[Path] = list(Path(CORPUS_DIR).rglob('*-*.txt'))
    random.shuffle(corpus)  # 記事をシャッフルします

    with open(QDRANT_JSON, 'w') as fp:
        for x in corpus:
            doc: Dict[str, str] = read_document(x)
            json.dump(doc, fp)  # 1行分
            fp.write('\n')


if __name__ == '__main__':
    load_dataset_from_livedoor_files()

$ python3 corpus.py
$ ls
corpus.py livedoor-corpus livedoor.json

このlivedoor.jsonをGoogle collaboで使います。

### Google colabo
!pip install -U ginza spacy
!pip install -U numpy pandas ja_ginza

colaboで文章をベクトル化 … 約10分

import numpy as np
import pandas as pd
import spacy
# from multiprocessing import Pool, cpu_count  <- マルチプロセス関連は不要

# GiNZAモデルのロード (インストールが完了している前提)
try:
    nlp: spacy.Language = spacy.load('ja_ginza', exclude=["tagger", "parser", "ner", "lemmatizer", "textcat", "custom"])
    print("✅ GiNZAモデルのロードに成功しました。")
except OSError:
    print("❌ GiNZAモデルが見つかりません。再度インストール手順を確認してください。")
    # ここでエラーになる場合は、!pip install -U ginza を実行してください。

QDRANT_NPY = 'vectors-livedoor-ginza.npy'  # 出力ファイル名

def f(x):
    # NaNやNone値のチェック (エラー回避のため)
    if pd.isna(x):
        # 空のベクトルを返す、または処理をスキップ
        return np.zeros(nlp.vocab.vectors_length)
        
    doc: spacy.tokens.doc.Doc = nlp(x)  # GiNZAでベクトル化
    return doc.vector

def main():
    try:
        df = pd.read_json('livedoor.json', lines=True)
    except FileNotFoundError:
        print("❌ livedoor.json が見つかりません。ファイルが /content/ にアップロードされているか確認してください。")
        return

    print("\nデータフレームの先頭5行:")
    print(df.head())
    print(f"\n合計 {len(df)} 件の文書をベクトル化中... (シングルプロセス)")
    
    # 修正箇所: df.body.apply(f) を使用してシングルプロセスでベクトル化
    vectors_list = df.body.apply(f).tolist()

    print("ベクトル化完了。NumPyファイルに保存中...")

    # リストをNumPy配列に変換して保存
    vectors_array = np.array(vectors_list)
    np.save(QDRANT_NPY, vectors_array, allow_pickle=False)
    
    print(f"\n========================================================")
    print(f"✅ 保存完了: {QDRANT_NPY}")
    print(f"配列の形状 (Shape): {vectors_array.shape}")
    print(f"========================================================")

# 処理の実行
main()

vectors-livedoor-ginza.npy ができます。

### Qdrantの使い方
### docker pull
$ sudo docker pull qdrant/qdrant
$ sudo docker run -p 6333:6333 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant
_ _
__ _ __| |_ __ __ _ _ __ | |_
/ _` |/ _` | ‘__/ _` | ‘_ \| __|
| (_| | (_| | | | (_| | | | | |_
\__, |\__,_|_| \__,_|_| |_|\__|
|_|

Version: 1.15.4, build: 20db14f8
Access web UI at http://localhost:6333/dashboard

2025-09-27T03:10:37.414937Z INFO storage::content_manager::consensus::persistent: Initializing new raft state at ./storage/raft_state.json
2025-09-27T03:10:37.427278Z INFO qdrant: Distributed mode disabled
2025-09-27T03:10:37.428004Z INFO qdrant: Telemetry reporting enabled, id: 4ab6c13b-1d33-4b1f-ac4a-baff31ff55ad
2025-09-27T03:10:37.460847Z INFO qdrant::actix: TLS disabled for REST API
2025-09-27T03:10:37.463591Z INFO qdrant::actix: Qdrant HTTP listening on 6333
2025-09-27T03:10:37.465943Z INFO actix_server::builder: starting 1 workers
2025-09-27T03:10:37.466167Z INFO actix_server::server: Actix runtime found; starting in Actix runtime
2025-09-27T03:10:37.466216Z INFO actix_server::server: starting service: “actix-web-service-0.0.0.0:6333”, workers: 1, listening on: 0.0.0.0:6333
2025-09-27T03:10:37.467991Z INFO qdrant::tonic: Qdrant gRPC listening on 6334
2025-09-27T03:10:37.468111Z INFO qdrant::tonic: TLS disabled for gRPC API

### SDKインストール
$ pip3 install qdrant-client

### Qdrantの説明
– コレクション: RDBテーブル
– ポイント: RDBレコード ポイントには、ペイロード(Payload)と呼ばれるメタ情報も一緒に登録できる。メタ情報はフィルター検索に使用する。

### コレクションの作成

from qdrant_client import QdrantClient
from qdrant_client.http.models import VectorParams, Distance 
collection_name = 'livedoor'
qdrant_client = QdrantClient(host='localhost', port=6333)

qdrant_client.recreate_collection(
        collection_name=collection_name,
        vectors_config=VectorParams(size=300, distance=Distance.COSINE) # GiNZAは300次元
    )

### コレクションの確認

from qdrant_client import QdrantClient

# Qdrantクライアントを起動中のDockerコンテナに接続
qdrant_client = QdrantClient(host='localhost', port=6333)

# 存在する全てのコレクション名を取得
collections = qdrant_client.get_collections()
collection_names = [c.name for c in collections.collections]

print("====================================")
print("✅ Qdrantに存在するコレクション:")

if 'livedoor' in collection_names:
    print(f"   [O] 'livedoor' コレクションが見つかりました。")
    
    # 詳細情報の取得でエラーが出るため、シンプルな情報に修正
    collection_info = qdrant_client.get_collection(collection_name='livedoor')
    print(f"   - ステータス: {collection_info.status.value}")
    print(f"   - ポイント数: {collection_info.points_count} (現在は0のはずです)")
else:
    print("   [X] 'livedoor' コレクションは見つかりませんでした。")
    print(f"   現在存在するコレクション: {collection_names}")
print("====================================")

$ python3 check_collection.py
====================================
✅ Qdrantに存在するコレクション:
[O] ‘livedoor’ コレクションが見つかりました。
– ステータス: green
– ポイント数: 0 (現在は0のはずです)
====================================

### upload

import json
import numpy as np
import pandas as pd
from qdrant_client import QdrantClient

# =========================================================================
# 1. 補助関数の定義 (JSONファイルの読み込み用)
# livedoor.jsonには不要なキーが含まれている可能性があるため、
# 必要なキーだけを抽出する目的の関数です。
# =========================================================================
def hook(obj):
    """
    JSONオブジェクトから必要なペイロードデータのみを抽出するフック関数。
    """
    if 'body' in obj:
        # 必要なキー(本文、タイトル、カテゴリ)を抽出して返す
        return {
            "title": obj.get("title", ""),
            "body": obj.get("body", ""),
            "category": obj.get("category", "")
        }
    return obj

# =========================================================================
# 2. メイン処理
# =========================================================================
def main():
    # 接続情報
    collection_name = 'livedoor'
    qdrant_client = QdrantClient(host='localhost', port=6333)

    # データの読み込み
    try:
        # ベクトルデータの読み込み
        vectors = np.load('./vectors-livedoor-ginza.npy')
        
        # JSONファイルの読み込みとペイロードの準備
        print("JSONファイルを読み込んでペイロードを準備中...")
        docs = []
        with open('./livedoor.json', 'r', encoding='utf-8') as fd:
            # 各行(一つのJSONオブジェクト)を読み込み、hook関数で必要なキーを抽出
            for line in fd:
                docs.append(json.loads(line, object_hook=hook))
        
        print(f"✅ 読み込み完了。ベクトル数: {vectors.shape[0]}、文書数: {len(docs)}")

    except FileNotFoundError as e:
        print(f"❌ ファイルが見つかりません: {e.filename}")
        print("ファイル(livedoor.json, vectors-livedoor-ginza.npy)が同じディレクトリにあるか確認してください。")
        return

    # コレクションへのアップロード
    print("Qdrantコレクションにデータをアップロード中...")
    qdrant_client.upload_collection(
        collection_name=collection_name, # コレクション名
        vectors=vectors, # ベクトルデータ (NumPy配列)
        payload=iter(docs), # ペイロードデータ (ジェネレータまたはイテレータ)
        ids=None,  # IDの自動発番
        batch_size=256  # バッチサイズ
    )
    print("✅ データアップロード完了。")
    
    # 最終確認
    collection_info = qdrant_client.get_collection(collection_name='livedoor')
    print(f"最終ポイント数: {collection_info.points_count}")
    
# スクリプトの実行
if __name__ == "__main__":
    main()

$ python3 upload_data.py
JSONファイルを読み込んでペイロードを準備中…
✅ 読み込み完了。ベクトル数: 7367、文書数: 7367
Qdrantコレクションにデータをアップロード中…
✅ データアップロード完了。
最終ポイント数: 7367

$ pip3 install spacy ginza

import numpy as np
import spacy
from qdrant_client import QdrantClient
from qdrant_client.http.models import ScoredPoint

# =========================================================================
# 1. 初期設定とモデルロード
# =========================================================================
# ベクトル化に使用したモデルと同じものをロード
# 以前のステップでインストールが完了していることを前提とします
try:
    nlp: spacy.Language = spacy.load('ja_ginza', exclude=["tagger", "parser", "ner", "lemmatizer", "textcat", "custom"])
    print("✅ GiNZAモデルのロードに成功しました。")
except OSError:
    print("❌ GiNZAモデルが見つかりません。")
    exit() # 処理を中断

# Qdrant接続情報
collection_name = 'livedoor'
qdrant_client = QdrantClient(host='localhost', port=6333)

# 検索クエリ
QUERY_TEXT = "男磨きの動画を見ています"

# =========================================================================
# 2. クエリテキストのベクトル化
# =========================================================================
def get_vector_from_text(text: str) -> np.ndarray:
    """
    GiNZAを使用してテキストをベクトルに変換します。
    """
    doc: spacy.tokens.doc.Doc = nlp(text)
    # GiNZAのdoc.vectorはNumPy配列を返します
    return doc.vector

# =========================================================================
# 3. Qdrantでの検索実行
# =========================================================================
def main():
    print(f"\n========================================================")
    print(f"🔍 検索クエリ: {QUERY_TEXT}")
    print(f"========================================================")

    # クエリテキストをベクトルに変換
    query_vector = get_vector_from_text(QUERY_TEXT)

    # Qdrantで検索を実行
    hits = qdrant_client.search(
        collection_name=collection_name,
        query_vector=query_vector, # ベクトル化したクエリー
        query_filter=None,
        with_payload=True, # レスポンスにペイロードを含める
        limit=5 # 上位5件を取得
    )
    
    # 検索結果の表示
    print("\n[検索結果 - 上位 5件]")
    if not hits:
        print("類似記事は見つかりませんでした。")
        return

    for i, hit in enumerate(hits):
        h: ScoredPoint = hit
        # ペイロードからタイトルと本文を取得
        title = h.payload.get('title', 'N/A')
        body_snippet = h.payload.get('body', 'N/A')[:100] + '...' # 本文は先頭100文字を抜粋
        
        print(f"--- 順位 {i+1} (スコア: {h.score:.4f}) ---")
        print(f"タイトル: {title}")
        print(f"本文抜粋: {body_snippet}")
        
# スクリプトの実行
if __name__ == "__main__":
    main()
import numpy as np
import spacy
from qdrant_client import QdrantClient
from qdrant_client.http.models import ScoredPoint

# =========================================================================
# 1. 初期設定とモデルロード
# =========================================================================
# ベクトル化に使用したモデルと同じものをロード
# 以前のステップでインストールが完了していることを前提とします
try:
    nlp: spacy.Language = spacy.load('ja_ginza', exclude=["tagger", "parser", "ner", "lemmatizer", "textcat", "custom"])
    print("✅ GiNZAモデルのロードに成功しました。")
except OSError:
    print("❌ GiNZAモデルが見つかりません。")
    exit() # 処理を中断

# Qdrant接続情報
collection_name = 'livedoor'
qdrant_client = QdrantClient(host='localhost', port=6333)

# 検索クエリ
QUERY_TEXT = "野球情報が知りたい"

# =========================================================================
# 2. クエリテキストのベクトル化
# =========================================================================
def get_vector_from_text(text: str) -> np.ndarray:
    """
    GiNZAを使用してテキストをベクトルに変換します。
    """
    doc: spacy.tokens.doc.Doc = nlp(text)
    # GiNZAのdoc.vectorはNumPy配列を返します
    return doc.vector

# =========================================================================
# 3. Qdrantでの検索実行
# =========================================================================
def main():
    print(f"\n========================================================")
    print(f"🔍 検索クエリ: {QUERY_TEXT}")
    print(f"========================================================")

    # クエリテキストをベクトルに変換
    query_vector = get_vector_from_text(QUERY_TEXT)

    # Qdrantで検索を実行
    hits = qdrant_client.search(
        collection_name=collection_name,
        query_vector=query_vector, # ベクトル化したクエリー
        query_filter=None,
        with_payload=True, # レスポンスにペイロードを含める
        limit=5 # 上位5件を取得
    )
    
    # 検索結果の表示
    print("\n[検索結果 - 上位 5件]")
    if not hits:
        print("類似記事は見つかりませんでした。")
        return

    for i, hit in enumerate(hits):
        h: ScoredPoint = hit
        # ペイロードからタイトルと本文を取得
        title = h.payload.get('title', 'N/A')
        body_snippet = h.payload.get('body', 'N/A')[:100] + '...' # 本文は先頭100文字を抜粋
        
        print(f"--- 順位 {i+1} (スコア: {h.score:.4f}) ---")
        print(f"タイトル: {title}")
        print(f"本文抜粋: {body_snippet}")
        
# スクリプトの実行
if __name__ == "__main__":
    main()