コミットを比較
1 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 0d7757b2df |
@@ -14,24 +14,11 @@ class GekanatorGamesController < ApplicationController
|
|||||||
question_count: answers.length,
|
question_count: answers.length,
|
||||||
answers:)
|
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
|
render json: { errors: game.errors.full_messages }, status: :unprocessable_entity
|
||||||
return
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def extra_questions
|
def extra_questions
|
||||||
@@ -45,12 +32,10 @@ class GekanatorGamesController < ApplicationController
|
|||||||
.where(kind: 'post_similarity', source: 'user_suggested')
|
.where(kind: 'post_similarity', source: 'user_suggested')
|
||||||
.to_a
|
.to_a
|
||||||
|
|
||||||
selected =
|
selected = weighted_sample_questions(
|
||||||
prioritized_extra_questions(
|
questions,
|
||||||
questions,
|
post_id: game.correct_post_id,
|
||||||
post_id: game.correct_post_id,
|
limit: 2)
|
||||||
user: current_user,
|
|
||||||
limit: 2)
|
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
questions: selected.map { |question| extra_question_json(question) }
|
questions: selected.map { |question| extra_question_json(question) }
|
||||||
@@ -111,23 +96,6 @@ class GekanatorGamesController < ApplicationController
|
|||||||
}
|
}
|
||||||
end
|
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:
|
def weighted_sample_questions questions, post_id:, limit:
|
||||||
remaining = questions.uniq(&:id)
|
remaining = questions.uniq(&:id)
|
||||||
selected = []
|
selected = []
|
||||||
@@ -177,85 +145,4 @@ class GekanatorGamesController < ApplicationController
|
|||||||
|
|
||||||
game
|
game
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class GekanatorPostsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
posts =
|
posts =
|
||||||
Post
|
Post
|
||||||
.preload(:post_similarities, tags: :tag_name)
|
.preload(tags: :tag_name)
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
.order(Arel.sql(
|
.order(Arel.sql(
|
||||||
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE, ' \
|
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE, ' \
|
||||||
@@ -22,12 +22,6 @@ class GekanatorPostsController < ApplicationController
|
|||||||
thumbnail_base: post.thumbnail_base,
|
thumbnail_base: post.thumbnail_base,
|
||||||
original_created_from: post.original_created_from,
|
original_created_from: post.original_created_from,
|
||||||
original_created_before: post.original_created_before,
|
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) }
|
tags: post.tags.map { |tag| tag_json(tag) }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,35 +8,6 @@ class GekanatorQuestionSuggestionsController < ApplicationController
|
|||||||
return head :not_found
|
return head :not_found
|
||||||
end
|
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(
|
suggestion = GekanatorQuestionSuggestion.new(
|
||||||
gekanator_game: game,
|
gekanator_game: game,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
@@ -82,14 +53,4 @@ class GekanatorQuestionSuggestionsController < ApplicationController
|
|||||||
rescue NotImplementedError
|
rescue NotImplementedError
|
||||||
head :not_implemented
|
head :not_implemented
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class GekanatorQuestionsController < ApplicationController
|
|||||||
def question_json question
|
def question_json question
|
||||||
condition = condition_json(question.condition).deep_symbolize_keys
|
condition = condition_json(question.condition).deep_symbolize_keys
|
||||||
json = {
|
json = {
|
||||||
record_id: question.id,
|
|
||||||
id: question_id_for(question, condition),
|
id: question_id_for(question, condition),
|
||||||
text: question_text_for(question, condition),
|
text: question_text_for(question, condition),
|
||||||
kind: question.kind,
|
kind: question.kind,
|
||||||
@@ -24,7 +23,7 @@ class GekanatorQuestionsController < ApplicationController
|
|||||||
source: question.source,
|
source: question.source,
|
||||||
priority_weight: question.priority_weight
|
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)
|
json[:example_answers] = example_answers_json(question)
|
||||||
end
|
end
|
||||||
json
|
json
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class GekanatorQuestionExample < ApplicationRecord
|
class GekanatorQuestionExample < ApplicationRecord
|
||||||
ANSWERS = GekanatorQuestionSuggestion::ANSWERS
|
ANSWERS = GekanatorQuestionSuggestion::ANSWERS
|
||||||
NON_UNKNOWN_ANSWERS = ANSWERS - ['unknown']
|
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 :gekanator_question
|
||||||
belongs_to :post
|
belongs_to :post
|
||||||
@@ -35,7 +35,7 @@ class GekanatorQuestionExample < ApplicationRecord
|
|||||||
self.answer_counts = counts
|
self.answer_counts = counts
|
||||||
self.sample_count = counts.values.sum
|
self.sample_count = counts.values.sum
|
||||||
self.gekanator_game = gekanator_game if gekanator_game.present?
|
self.gekanator_game = gekanator_game if gekanator_game.present?
|
||||||
self.source = source
|
self.source = source if new_record?
|
||||||
|
|
||||||
apply_aggregated_answer!(preferred_answer: answer)
|
apply_aggregated_answer!(preferred_answer: answer)
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class GekanatorQuestionSuggestion < ApplicationRecord
|
class GekanatorQuestionSuggestion < ApplicationRecord
|
||||||
|
MAX_QUESTIONS_PER_GAME = 3
|
||||||
ANSWERS = ['yes', 'no', 'partial', 'probably_no', 'unknown'].freeze
|
ANSWERS = ['yes', 'no', 'partial', 'probably_no', 'unknown'].freeze
|
||||||
|
|
||||||
belongs_to :gekanator_game
|
belongs_to :gekanator_game
|
||||||
@@ -9,4 +10,16 @@ class GekanatorQuestionSuggestion < ApplicationRecord
|
|||||||
validates :question_text, presence: true, length: { maximum: 1000 }
|
validates :question_text, presence: true, length: { maximum: 1000 }
|
||||||
validates :answer, presence: true, inclusion: { in: ANSWERS }
|
validates :answer, presence: true, inclusion: { in: ANSWERS }
|
||||||
validates :processed, inclusion: { in: [true, false] }
|
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
|
end
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export type GekanatorExtraQuestion = {
|
|||||||
priorityWeight: number }
|
priorityWeight: number }
|
||||||
|
|
||||||
export type StoredGekanatorQuestion = {
|
export type StoredGekanatorQuestion = {
|
||||||
recordId?: number
|
|
||||||
id: string
|
id: string
|
||||||
text: string
|
text: string
|
||||||
kind: GekanatorQuestionKind
|
kind: GekanatorQuestionKind
|
||||||
@@ -72,7 +71,6 @@ export type StoredGekanatorQuestion = {
|
|||||||
exampleAnswers?: Record<`${ number }`, GekanatorAnswerValue> }
|
exampleAnswers?: Record<`${ number }`, GekanatorAnswerValue> }
|
||||||
|
|
||||||
export type GekanatorQuestion = {
|
export type GekanatorQuestion = {
|
||||||
recordId?: number
|
|
||||||
id: string
|
id: string
|
||||||
text: string
|
text: string
|
||||||
kind: GekanatorQuestionKind
|
kind: GekanatorQuestionKind
|
||||||
@@ -150,7 +148,7 @@ const directExampleAnswerFor = (
|
|||||||
question: StoredGekanatorQuestion,
|
question: StoredGekanatorQuestion,
|
||||||
post: Post,
|
post: Post,
|
||||||
): GekanatorAnswerValue | null => {
|
): GekanatorAnswerValue | null => {
|
||||||
if (question.kind !== 'post_similarity' && question.kind !== 'tag')
|
if (question.kind !== 'post_similarity')
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const direct = question.exampleAnswers?.[String (post.id) as `${ number }`]
|
const direct = question.exampleAnswers?.[String (post.id) as `${ number }`]
|
||||||
@@ -350,7 +348,6 @@ export const restoreGekanatorQuestion = (
|
|||||||
const normalizedCondition = normalizeTitleLengthCondition (question.condition)
|
const normalizedCondition = normalizeTitleLengthCondition (question.condition)
|
||||||
const normalizedQuestion = {
|
const normalizedQuestion = {
|
||||||
...question,
|
...question,
|
||||||
recordId: question.recordId,
|
|
||||||
id: normalizedCondition.type === 'title-length-at-least'
|
id: normalizedCondition.type === 'title-length-at-least'
|
||||||
? `title:length-at-least:${ normalizedCondition.length }`
|
? `title:length-at-least:${ normalizedCondition.length }`
|
||||||
: question.id,
|
: question.id,
|
||||||
@@ -370,7 +367,6 @@ export const storeGekanatorQuestion = (
|
|||||||
id: question.condition.type === 'title-length-greater-than'
|
id: question.condition.type === 'title-length-greater-than'
|
||||||
? `title:length-at-least:${ question.condition.length + 1 }`
|
? `title:length-at-least:${ question.condition.length + 1 }`
|
||||||
: question.id,
|
: question.id,
|
||||||
recordId: question.recordId,
|
|
||||||
text: question.text,
|
text: question.text,
|
||||||
kind: question.kind,
|
kind: question.kind,
|
||||||
condition: normalizeTitleLengthCondition (question.condition),
|
condition: normalizeTitleLengthCondition (question.condition),
|
||||||
@@ -585,7 +581,7 @@ export const saveGekanatorGame = async ({
|
|||||||
guessedPostId: number
|
guessedPostId: number
|
||||||
correctPostId: number
|
correctPostId: number
|
||||||
answers: GekanatorAnswerLog[]
|
answers: GekanatorAnswerLog[]
|
||||||
}): Promise<{ id: number; learnedExampleCount: number }> =>
|
}): Promise<{ id: number }> =>
|
||||||
await apiPost ('/gekanator/games', {
|
await apiPost ('/gekanator/games', {
|
||||||
guessed_post_id: guessedPostId,
|
guessed_post_id: guessedPostId,
|
||||||
correct_post_id: correctPostId,
|
correct_post_id: correctPostId,
|
||||||
@@ -599,18 +595,15 @@ export const saveGekanatorGame = async ({
|
|||||||
|
|
||||||
export const saveGekanatorQuestionSuggestion = async ({
|
export const saveGekanatorQuestionSuggestion = async ({
|
||||||
gekanatorGameId,
|
gekanatorGameId,
|
||||||
existingQuestionId,
|
|
||||||
questionText,
|
questionText,
|
||||||
answer,
|
answer,
|
||||||
}: {
|
}: {
|
||||||
gekanatorGameId: number
|
gekanatorGameId: number
|
||||||
existingQuestionId?: number
|
questionText: string
|
||||||
questionText?: string
|
|
||||||
answer: GekanatorAnswerValue
|
answer: GekanatorAnswerValue
|
||||||
}): Promise<{ id: number; count: number }> =>
|
}): Promise<{ id: number; count: number }> =>
|
||||||
await apiPost ('/gekanator/question_suggestions', {
|
await apiPost ('/gekanator/question_suggestions', {
|
||||||
gekanator_game_id: gekanatorGameId,
|
gekanator_game_id: gekanatorGameId,
|
||||||
existing_question_id: existingQuestionId,
|
|
||||||
question_text: questionText,
|
question_text: questionText,
|
||||||
answer })
|
answer })
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,6 @@ export type RecoveredCandidatePost = {
|
|||||||
answerCountAtRecovery: number }
|
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 = ({
|
export const candidatePostsFor = ({
|
||||||
posts,
|
posts,
|
||||||
questions,
|
questions,
|
||||||
@@ -56,8 +46,6 @@ export const candidatePostsFor = ({
|
|||||||
const question = questionById.get (answer.questionId)
|
const question = questionById.get (answer.questionId)
|
||||||
if (!(question))
|
if (!(question))
|
||||||
return true
|
return true
|
||||||
if (!(questionIsFactLikeForHardFiltering (question)))
|
|
||||||
return true
|
|
||||||
|
|
||||||
switch (answer.answer)
|
switch (answer.answer)
|
||||||
{
|
{
|
||||||
@@ -83,9 +71,6 @@ export const hardFilteredPostsForAnswer = ({
|
|||||||
question: GekanatorQuestion
|
question: GekanatorQuestion
|
||||||
answer: GekanatorAnswerValue
|
answer: GekanatorAnswerValue
|
||||||
}): Post[] => {
|
}): Post[] => {
|
||||||
if (!(questionIsFactLikeForHardFiltering (question)))
|
|
||||||
return posts
|
|
||||||
|
|
||||||
if (answer === 'unknown')
|
if (answer === 'unknown')
|
||||||
return posts
|
return posts
|
||||||
|
|
||||||
|
|||||||
+507
-734
ファイル差分が大きすぎるため省略します
差分を読込み
@@ -139,10 +139,6 @@ export type Post = {
|
|||||||
title: string | null
|
title: string | null
|
||||||
thumbnail: string | null
|
thumbnail: string | null
|
||||||
thumbnailBase: string | null
|
thumbnailBase: string | null
|
||||||
postSimilarityEdges?: {
|
|
||||||
targetPostId: number
|
|
||||||
cos: number
|
|
||||||
}[]
|
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
parentPosts?: Post[]
|
parentPosts?: Post[]
|
||||||
childPosts?: Post[]
|
childPosts?: Post[]
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする