コミットを比較

..

1 コミット

作成者 SHA1 メッセージ 日付
みてるぞ 0d7757b2df #370 2026-06-15 22:09:10 +09:00
10個のファイルの変更534行の追加933行の削除
+7 -120
ファイルの表示
@@ -14,24 +14,11 @@ class GekanatorGamesController < ApplicationController
question_count: answers.length,
answers:)
if game.invalid?
if game.save
render json: { id: game.id }, status: :created
else
render json: { errors: game.errors.full_messages }, status: :unprocessable_entity
return
end
learned_example_count = 0
ActiveRecord::Base.transaction do
game.save!
learned_example_count = learn_answers_from_game!(game)
end
render json: {
id: game.id,
learned_example_count:
}, status: :created
rescue ActiveRecord::RecordInvalid => e
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
end
def extra_questions
@@ -45,12 +32,10 @@ class GekanatorGamesController < ApplicationController
.where(kind: 'post_similarity', source: 'user_suggested')
.to_a
selected =
prioritized_extra_questions(
questions,
post_id: game.correct_post_id,
user: current_user,
limit: 2)
selected = weighted_sample_questions(
questions,
post_id: game.correct_post_id,
limit: 2)
render json: {
questions: selected.map { |question| extra_question_json(question) }
@@ -111,23 +96,6 @@ class GekanatorGamesController < ApplicationController
}
end
def prioritized_extra_questions questions, post_id:, user:, limit:
answered_question_ids =
GekanatorQuestionExample
.where(user:, gekanator_question_id: questions.map(&:id))
.distinct
.pluck(:gekanator_question_id)
unanswered, answered =
questions.partition { |question| !answered_question_ids.include?(question.id) }
selected = weighted_sample_questions(unanswered, post_id:, limit:)
return selected if selected.length >= limit
selected + weighted_sample_questions(
answered.reject { |question| selected.any? { _1.id == question.id } },
post_id:,
limit: limit - selected.length)
end
def weighted_sample_questions questions, post_id:, limit:
remaining = questions.uniq(&:id)
selected = []
@@ -177,85 +145,4 @@ class GekanatorGamesController < ApplicationController
game
end
def learn_answers_from_game! game
correct_post = game.correct_post
return 0 if correct_post.blank?
accepted_questions =
GekanatorQuestion
.accepted
.index_by { |question| public_question_id_for(question) }
learned_count = 0
Array(game.answers).each do |answer|
answer_value = answer['answer'].to_s
next if answer_value.blank? || answer_value == 'unknown'
question = accepted_questions[answer['question_id'].to_s]
next unless learnable_game_answer_question?(question)
example =
GekanatorQuestionExample.find_or_initialize_by(
gekanator_question: question,
post: correct_post,
user: current_user)
example.record_answer!(
answer: answer_value,
source: 'post_game_answer',
gekanator_game: game)
example.save!
learned_count += 1
end
learned_count
end
def public_question_id_for question
condition = normalize_condition(question.condition)
case condition[:type]
when 'tag'
"tag:#{condition[:key]}"
when 'source'
"source:#{condition[:host]}"
when 'original-year'
"original-year:#{condition[:year]}"
when 'original-month'
"original-month:#{condition[:month]}"
when 'original-month-day'
"original-month-day:#{condition[:monthDay] || condition[:month_day]}"
when 'title-length-at-least'
"title:length-at-least:#{condition[:length]}"
when 'title-length-greater-than'
"title:length-at-least:#{condition[:length].to_i + 1}"
when 'title-has-ascii'
'title:ascii'
when 'title-contains'
"title:contains:#{condition[:text]}"
when 'post-similarity'
"post-similarity:#{question.id}"
else
"catalog:#{question.id}"
end
end
def normalize_condition condition
json = condition.deep_dup.as_json
if json['type'] == 'original-month-day' && json['monthDay'].blank?
json['monthDay'] = json.delete('month_day')
end
json.deep_symbolize_keys
end
def learnable_game_answer_question? question
return true if question.kind == 'post_similarity'
return false unless question.kind == 'tag'
condition = normalize_condition(question.condition)
key = condition[:key].to_s
!key.start_with?('nico:')
end
end
+1 -7
ファイルの表示
@@ -2,7 +2,7 @@ class GekanatorPostsController < ApplicationController
def index
posts =
Post
.preload(:post_similarities, tags: :tag_name)
.preload(tags: :tag_name)
.with_attached_thumbnail
.order(Arel.sql(
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE, ' \
@@ -22,12 +22,6 @@ class GekanatorPostsController < ApplicationController
thumbnail_base: post.thumbnail_base,
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,
post_similarity_edges: post.post_similarities.map { |similarity|
{
target_post_id: similarity.target_post_id,
cos: similarity.cos.to_f
}
},
tags: post.tags.map { |tag| tag_json(tag) }
}
end
-39
ファイルの表示
@@ -8,35 +8,6 @@ class GekanatorQuestionSuggestionsController < ApplicationController
return head :not_found
end
existing_question_id = params[:existing_question_id].presence
if existing_question_id
question = GekanatorQuestion.accepted.find_by(id: existing_question_id)
return head :not_found unless question
unless learnable_existing_question?(question)
return render_validation_error fields: { existing_question_id: ['質問が不正です.'] }
end
example =
GekanatorQuestionExample.find_or_initialize_by(
gekanator_question: question,
post: game.correct_post,
user: current_user)
example.record_answer!(
answer: params.require(:answer),
source: 'post_game_extra',
gekanator_game: game)
if example.save
render json: {
id: question.id,
count: game.question_suggestions.count
}, status: :created
else
render_validation_error example
end
return
end
suggestion = GekanatorQuestionSuggestion.new(
gekanator_game: game,
user: current_user,
@@ -82,14 +53,4 @@ class GekanatorQuestionSuggestionsController < ApplicationController
rescue NotImplementedError
head :not_implemented
end
private
def learnable_existing_question? question
return true if question.kind == 'post_similarity'
return false unless question.kind == 'tag'
key = question.condition.as_json['key'].to_s
!key.start_with?('nico:')
end
end
+1 -2
ファイルの表示
@@ -16,7 +16,6 @@ class GekanatorQuestionsController < ApplicationController
def question_json question
condition = condition_json(question.condition).deep_symbolize_keys
json = {
record_id: question.id,
id: question_id_for(question, condition),
text: question_text_for(question, condition),
kind: question.kind,
@@ -24,7 +23,7 @@ class GekanatorQuestionsController < ApplicationController
source: question.source,
priority_weight: question.priority_weight
}
if question.kind == 'post_similarity' || question.kind == 'tag'
if question.kind == 'post_similarity'
json[:example_answers] = example_answers_json(question)
end
json
+2 -2
ファイルの表示
@@ -1,7 +1,7 @@
class GekanatorQuestionExample < ApplicationRecord
ANSWERS = GekanatorQuestionSuggestion::ANSWERS
NON_UNKNOWN_ANSWERS = ANSWERS - ['unknown']
SOURCES = ['initial_suggestion', 'post_game_answer', 'post_game_extra'].freeze
SOURCES = ['initial_suggestion', 'post_game_extra'].freeze
belongs_to :gekanator_question
belongs_to :post
@@ -35,7 +35,7 @@ class GekanatorQuestionExample < ApplicationRecord
self.answer_counts = counts
self.sample_count = counts.values.sum
self.gekanator_game = gekanator_game if gekanator_game.present?
self.source = source
self.source = source if new_record?
apply_aggregated_answer!(preferred_answer: answer)
self
+13
ファイルの表示
@@ -1,4 +1,5 @@
class GekanatorQuestionSuggestion < ApplicationRecord
MAX_QUESTIONS_PER_GAME = 3
ANSWERS = ['yes', 'no', 'partial', 'probably_no', 'unknown'].freeze
belongs_to :gekanator_game
@@ -9,4 +10,16 @@ class GekanatorQuestionSuggestion < ApplicationRecord
validates :question_text, presence: true, length: { maximum: 1000 }
validates :answer, presence: true, inclusion: { in: ANSWERS }
validates :processed, inclusion: { in: [true, false] }
validate :question_suggestion_limit_per_game, on: :create
private
def question_suggestion_limit_per_game
return if gekanator_game_id.blank?
count = GekanatorQuestionSuggestion.where(gekanator_game_id:).count
if count >= MAX_QUESTIONS_PER_GAME
errors.add(:base, '質問追加数を超えてゐます.')
end
end
end
+3 -10
ファイルの表示
@@ -62,7 +62,6 @@ export type GekanatorExtraQuestion = {
priorityWeight: number }
export type StoredGekanatorQuestion = {
recordId?: number
id: string
text: string
kind: GekanatorQuestionKind
@@ -72,7 +71,6 @@ export type StoredGekanatorQuestion = {
exampleAnswers?: Record<`${ number }`, GekanatorAnswerValue> }
export type GekanatorQuestion = {
recordId?: number
id: string
text: string
kind: GekanatorQuestionKind
@@ -150,7 +148,7 @@ const directExampleAnswerFor = (
question: StoredGekanatorQuestion,
post: Post,
): GekanatorAnswerValue | null => {
if (question.kind !== 'post_similarity' && question.kind !== 'tag')
if (question.kind !== 'post_similarity')
return null
const direct = question.exampleAnswers?.[String (post.id) as `${ number }`]
@@ -350,7 +348,6 @@ export const restoreGekanatorQuestion = (
const normalizedCondition = normalizeTitleLengthCondition (question.condition)
const normalizedQuestion = {
...question,
recordId: question.recordId,
id: normalizedCondition.type === 'title-length-at-least'
? `title:length-at-least:${ normalizedCondition.length }`
: question.id,
@@ -370,7 +367,6 @@ export const storeGekanatorQuestion = (
id: question.condition.type === 'title-length-greater-than'
? `title:length-at-least:${ question.condition.length + 1 }`
: question.id,
recordId: question.recordId,
text: question.text,
kind: question.kind,
condition: normalizeTitleLengthCondition (question.condition),
@@ -585,7 +581,7 @@ export const saveGekanatorGame = async ({
guessedPostId: number
correctPostId: number
answers: GekanatorAnswerLog[]
}): Promise<{ id: number; learnedExampleCount: number }> =>
}): Promise<{ id: number }> =>
await apiPost ('/gekanator/games', {
guessed_post_id: guessedPostId,
correct_post_id: correctPostId,
@@ -599,18 +595,15 @@ export const saveGekanatorGame = async ({
export const saveGekanatorQuestionSuggestion = async ({
gekanatorGameId,
existingQuestionId,
questionText,
answer,
}: {
gekanatorGameId: number
existingQuestionId?: number
questionText?: string
questionText: string
answer: GekanatorAnswerValue
}): Promise<{ id: number; count: number }> =>
await apiPost ('/gekanator/question_suggestions', {
gekanator_game_id: gekanatorGameId,
existing_question_id: existingQuestionId,
question_text: questionText,
answer })
-15
ファイルの表示
@@ -13,16 +13,6 @@ export type RecoveredCandidatePost = {
answerCountAtRecovery: number }
const questionIsFactLikeForHardFiltering = (
question: GekanatorQuestion,
): boolean =>
!(question.kind === 'post_similarity'
|| (
question.kind === 'tag'
&& question.condition.type === 'tag'
&& !(question.condition.key.startsWith ('nico:'))))
export const candidatePostsFor = ({
posts,
questions,
@@ -56,8 +46,6 @@ export const candidatePostsFor = ({
const question = questionById.get (answer.questionId)
if (!(question))
return true
if (!(questionIsFactLikeForHardFiltering (question)))
return true
switch (answer.answer)
{
@@ -83,9 +71,6 @@ export const hardFilteredPostsForAnswer = ({
question: GekanatorQuestion
answer: GekanatorAnswerValue
}): Post[] => {
if (!(questionIsFactLikeForHardFiltering (question)))
return posts
if (answer === 'unknown')
return posts
ファイル差分が大きすぎるため省略します 差分を読込み
-4
ファイルの表示
@@ -139,10 +139,6 @@ export type Post = {
title: string | null
thumbnail: string | null
thumbnailBase: string | null
postSimilarityEdges?: {
targetPostId: number
cos: number
}[]
tags: Tag[]
parentPosts?: Post[]
childPosts?: Post[]