昨日に引き続いてザザっと実装した。不安を払拭したかったのでいちばん手間そうなところから終わらせることにした。

前回まで


前回まで

作りかけているやつ


lv-fox

今日の進捗


AND検索をやった。また、Gooleの検索結果みたいに検索文字の前後を表示したかったので入力された単語の前後8文字を含めて切り取って返す処理も入れた。8文字は特別意味はない。5は短いけど10は長いので8くらいという感じ。

この辺り気合いれてやってみたけど、例えばWordpressの検索結果とかはこんな手間なことはやってなくて、単純に該当記事だけを返すようにしてるみたいで「う~~ん、そこまで頑張って実装するものでもないのかなぁ…」と思った。

例えば/search?q=した&q=のでみたいなクエリパラメータを指定すると、したのでの両方を含む記事のうち、記事内から各々の単語の前後8文字部分を切り取って返す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
{
"url":"https://yoshinorin.net/2020/07/01/monthly-report/",
"title":"月報(2020年6月度)",
"content":"定に依存しているのでシステムの言語を... HIMA)が出るので、それまでに終わ... ",
"publishedAt":1593609615,
"updatedAt":1593615733
},
{
"url":"https://yoshinorin.net/2020/07/01/watch-movie-etc-2020-05-06/",
"title":"2020年5~6月に観た映画とか",
"content":"パチが観たかったので観た。満足。 感... ドンパチが観たいのでしばらくドンパチ... ",
"publishedAt":1593609607,
"updatedAt":1593612718
},
{
"url":"https://yoshinorin.net/2020/07/05/buy-food-processor/",
"title":"YAMAMOTO マルチスピードミキサー Master Cut を買った",
"content":"るが置き場がないので仕方ない。もっと... きという気がするので来週くらいに不動... ",
"publishedAt":1593932714,
"updatedAt":1594001665
}
]

クエリパラメータは+で繋いで指定するようにしたかったけど、なんかできなさそう??なので諦めた。

AND検索の実装


AND検索は入力された単語数の数だけlike句を動的に生成して検索するようにした。記事本文とタイトルを検索対象にしたかったけど、使用しているDBライブラリのquillで両方のカラムに対してlikeでOR検索する方法がわからず、記事本文のみ対象にすることにした。コードはこんな感じになった。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// DBのテーブルとマッピングされる case class
final case class Pages(
url: String,
title: String,
content: String,
publishedAt: Long,
updatedAt: Long
)

// DBライブラリ quillのコンテキスト
import ctx._

def find(words: List[String]): Seq[Pages] = {
// 入力された単語数の数だけlike句を動的に生成して検索
val q = queryBuilder(
words.tail,
0,
query[Pages].filter(p => p.content like lift(s"%${words.head}%"))
)
ctx.run(q)
}

@tailrec
private def queryBuilder(words: List[String], idx: Int = 0, acc: Quoted[Query[Pages]]): Quoted[Query[Pages]] = {
words.lift(idx) match {
case None => acc
case Some(w) =>
queryBuilder(
words,
idx + 1,
acc.filter(p => p.content like lift(s"%$w%"))
)
}
}

とにかく手を抜く方針なので生成のところのテーブル(case classを指定するところ)とかジェネリクスではなくベタで指定している(そもそもテーブル一つしかないけど)し、再帰のaccの初期値ももっとやりようがあるけどめんどくさいのでこれでいく。

念のため吐かれるSQLを確認しておきたいのでログを見ようかと思ったが、Windowsでログがどこに出力されているのわからず明日以降にやりたい。

入力された単語の前後n文字を含めて切り取る実装


入力された複数の単語それぞれの前後8文字を含めて切り取る実装は結構めんどくさかった。

文字全文から複数の単語の出現個所を取得する処理は、はじめは再帰でやろうかと思っていたけどstackoverflowに良い感じの回答があった。それをベースにすると思っていた以上に簡単にできた。

が、単語を複数で検索を掛けたときに重複するような結果がかえってきてしまうことがあるのが悩ましい。複数の単語が文章中で近しい位置に存在すると発生してしまう。しょうがないのでcreateNewSeqという重複回避のための(中途半端な)メソッドを実装した。これは本当に中途半端でとかとか頻出しそうな文字を複数入力して検索すると重複した結果がかえってきやすくなる。完璧に対応しようと思うと自分のアルゴリズム能力だと数時間とかではおさまらないので妥協することにした。入力値にある程度の制限を設けるつもりなので、実際このメソッドでほぼ防げると思う。

createNewSeqに関しては我ながらもっと綺麗に書けないのかとおもうけど書けないのでこれも諦める。これ絶対後でなにしたかったのかわかんなくなるだろうな…。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
val sentence = "本日はお日柄もよく。宴もたけなわなのですがほげほげほげげげげのげ。が、しかし...."
val idxes = createNewSeq(getAllWordsPosition(List("は", "が"), sentence))
println(substrRecursively(sentence, idxes))
// 本日はお日柄もよく。宴... たけなわなのですがほげほげほげげげ... ほげげげげのげ。が、しかし....

private def getAllWordsPosition(words: List[String], sentence: String): Seq[(Int, Int)] =
words.flatMap(_.r.findAllMatchIn(sentence).map(s => (s.start - 8, s.end + 8)).toSeq).sortBy(_._1)

@tailrec
private def createNewSeq(idxes: Seq[(Int, Int)], acc: Seq[(Int, Int)] = Nil): Seq[(Int, Int)] = {
if (idxes.nonEmpty) {
val i = idxes.head
idxes.lift(1) match {
case None =>
if (acc == Nil) createNewSeq(idxes.tail, Seq(idxes.head))
else acc
case Some(n) =>
if (n._1 > i._2) {
createNewSeq(idxes.tail, Seq(i, n))
} else {
if (acc.size > 1 && acc.tail.head._1 == i._1) createNewSeq(idxes.tail, acc.dropRight(1) :+ (i._1, n._2))
else createNewSeq(idxes.tail, acc :+ (i._1, n._2))
}
}
} else {
acc
}
}

@tailrec
private def substrRecursively(sentence: String, idxes: Seq[(Int, Int)], current: Int = 0, acc: String = ""): String = {
if (idxes.size > current) {
val i = idxes(current)
substrRecursively(
sentence,
idxes,
current + 1,
acc + sentence.slice(i._1, i._2) + "... "
)
} else {
acc
}
}

感想


えらい人もとりあえず動かせといってるので動かすところまで持っていく。これの実装やってるとおちおちゲームも他の勉強もできないので早いところ稼働させてしまいたい。7月中とかいうのんびりしたことを言わず、短期決戦を目指す。