Skip to content

Commit

Permalink
implement FindSimilarQuestions
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Apr 3, 2024
1 parent f3e8755 commit c4605db
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 8 deletions.
30 changes: 27 additions & 3 deletions MyApp.ServiceInterface/QuestionServices.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Net;
using System.Text.RegularExpressions;
using MyApp.Data;
using ServiceStack;
using MyApp.ServiceModel;
using ServiceStack.IO;
using ServiceStack.OrmLite;
using ServiceStack.Text;

namespace MyApp.ServiceInterface;

Expand All @@ -24,12 +24,12 @@ private List<string> ValidateQuestionTags(List<string>? tags)
throw new ArgumentException("Maximum of 5 tags allowed", nameof(tags));
return validTags;
}

public async Task<object> Get(GetAllAnswers request)
{
var question = await questions.GetQuestionAsync(request.Id);
var modelNames = question.Question?.Answers.Where(x => !string.IsNullOrEmpty(x.Model)).Select(x => x.Model).ToList();
var humanAnswers = question.Question?.Answers.Where(x => string.IsNullOrEmpty(x.Model)).Select(x => x.Id.SplitOnFirst("-")[1]).ToList();
var humanAnswers = question.Question?.Answers.Where(x => string.IsNullOrEmpty(x.Model)).Select(x => x.Id.LeftPart("-")).ToList();
modelNames?.AddRange(humanAnswers ?? []);
var answers = question
.GetAnswerFiles()
Expand All @@ -41,6 +41,30 @@ public async Task<object> Get(GetAllAnswers request)
};
}

static Regex AlphaNumericRegex = new("[^a-zA-Z0-9]", RegexOptions.Compiled);
static Regex SingleWhiteSpaceRegex = new( @"\s+", RegexOptions.Multiline | RegexOptions.Compiled);

public async Task<object> Any(FindSimilarQuestions request)
{
var searchPhrase = AlphaNumericRegex.Replace(request.Text, " ");
searchPhrase = SingleWhiteSpaceRegex.Replace(searchPhrase, " ").Trim();
if (searchPhrase.Length < 15)
throw new ArgumentException("Search text must be at least 20 characters", nameof(request.Text));

using var dbSearch = HostContext.AppHost.GetDbConnection(Databases.Search);
var q = dbSearch.From<PostFts>()
.Where("Body match {0} AND instr(RefId,'-') == 0", searchPhrase)
.OrderBy("rank")
.Limit(10);

var results = await dbSearch.SelectAsync(q);
var posts = await Db.PopulatePostsAsync(results);

return new FindSimilarQuestionsResponse
{
Results = posts
};
}

public async Task<object> Any(AskQuestion request)
{
Expand Down
12 changes: 12 additions & 0 deletions MyApp.ServiceModel/Posts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,18 @@ public class GetQuestionFile : IGet, IReturn<string>
public int Id { get; set; }
}

public class FindSimilarQuestions : IGet, IReturn<FindSimilarQuestionsResponse>
{
[ValidateNotEmpty, ValidateMinimumLength(20)]
public string Text { get; set; }
}

public class FindSimilarQuestionsResponse
{
public List<Post> Results { get; set; }
public ResponseStatus? ResponseStatus { get; set; }
}

[ValidateIsAuthenticated]
public class AskQuestion : IPost, IReturn<AskQuestionResponse>
{
Expand Down
43 changes: 42 additions & 1 deletion MyApp/wwwroot/mjs/dtos.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* Options:
Date: 2024-03-29 23:35:25
Date: 2024-04-03 11:09:35
Version: 8.22
Tip: To override a DTO option, remove "//" prefix before updating
BaseUrl: https://localhost:5001
Expand Down Expand Up @@ -374,6 +374,14 @@ export class CalculateLeaderboardResponse {
/** @type {ModelWinRate[]} */
modelWinRate;
}
export class FindSimilarQuestionsResponse {
/** @param {{results?:Post[],responseStatus?:ResponseStatus}} [init] */
constructor(init) { Object.assign(this, init) }
/** @type {Post[]} */
results;
/** @type {?ResponseStatus} */
responseStatus;
}
export class AskQuestionResponse {
/** @param {{id?:number,slug?:string,redirectTo?:string,responseStatus?:ResponseStatus}} [init] */
constructor(init) { Object.assign(this, init) }
Expand Down Expand Up @@ -601,6 +609,15 @@ export class CalculateLeaderBoard {
getMethod() { return 'GET' }
createResponse() { return new CalculateLeaderboardResponse() }
}
export class FindSimilarQuestions {
/** @param {{text?:string}} [init] */
constructor(init) { Object.assign(this, init) }
/** @type {string} */
text;
getTypeName() { return 'FindSimilarQuestions' }
getMethod() { return 'GET' }
createResponse() { return new FindSimilarQuestionsResponse() }
}
export class AskQuestion {
/** @param {{title?:string,body?:string,tags?:string[],refId?:string}} [init] */
constructor(init) { Object.assign(this, init) }
Expand Down Expand Up @@ -712,6 +729,17 @@ export class CreateWorkerAnswer {
getMethod() { return 'POST' }
createResponse() { return new IdResponse() }
}
export class RankAnswers {
/** @param {{postId?:number,votes?:{ [index: string]: number; }}} [init] */
constructor(init) { Object.assign(this, init) }
/** @type {number} */
postId;
/** @type {{ [index: string]: number; }} */
votes;
getTypeName() { return 'RankAnswers' }
getMethod() { return 'POST' }
createResponse() { return new IdResponse() }
}
export class CreateComment {
/** @param {{id?:string,body?:string}} [init] */
constructor(init) { Object.assign(this, init) }
Expand Down Expand Up @@ -791,6 +819,19 @@ export class PostVote {
getMethod() { return 'POST' }
createResponse() { }
}
export class CreateAvatar {
/** @param {{userName?:string,textColor?:string,bgColor?:string}} [init] */
constructor(init) { Object.assign(this, init) }
/** @type {string} */
userName;
/** @type {?string} */
textColor;
/** @type {?string} */
bgColor;
getTypeName() { return 'CreateAvatar' }
getMethod() { return 'GET' }
createResponse() { return '' }
}
export class RenderComponent {
/** @param {{ifQuestionModified?:number,regenerateMeta?:number,question?:QuestionAndAnswers,home?:RenderHome}} [init] */
constructor(init) { Object.assign(this, init) }
Expand Down
53 changes: 49 additions & 4 deletions MyApp/wwwroot/pages/Questions/Ask.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { ref, watchEffect, nextTick, onMounted } from "vue"
import { queryString } from "@servicestack/client"
import { useClient, useUtils } from "@servicestack/vue"
import { AskQuestion, PreviewMarkdown } from "dtos.mjs"
import { AskQuestion, PreviewMarkdown, FindSimilarQuestions } from "dtos.mjs"

export default {
template:`
<AutoForm ref="autoform" type="AskQuestion" v-model="request" header-class="" submit-label="Create Question"
:configureField="configureField" @success="onSuccess">
<template #heading></template>
<template #footer>
<div v-if="similarQuestions.length" class="px-6">
<div class="px-4 pb-2 bg-gray-50 dark:bg-gray-900 rounded-md">
<div class="flex justify-between items-center">
<h3 class="my-4 select-none text-xl font-semibold flex items-center cursor-pointer" @click="expandSimilar=!expandSimilar">
<svg :class="['w-4 h-4 inline-block mr-1 transition-all',!expandSimilar ? '-rotate-90' : '']" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M11.178 19.569a.998.998 0 0 0 1.644 0l9-13A.999.999 0 0 0 21 5H3a1.002 1.002 0 0 0-.822 1.569z"/></svg>
Similar Questions
</h3>
<span class="text-sm text-gray-500">has this been asked before?</span>
</div>
<div v-if="expandSimilar" class="pl-4">
<div v-for="q in similarQuestions" :key="q.id" class="pb-2">
<a :href="'/questions/' + q.id + '/' + q.slug" target="_blank" class="text-indigo-600 dark:text-indigo-300 hover:text-indigo-800">{{ q.title }}</a>
</div>
</div>
</div>
</div>
</template>
</AutoForm>
<div v-if="request.body" class="pb-40">
<h3 class="my-4 text-xl font-semibold">Preview</h3>
Expand All @@ -20,14 +38,18 @@ export default {

const client = useClient()
const autoform = ref()
const request = ref(new AskQuestion())
const savedJson = localStorage.getItem('ask')
const saved = savedJson ? JSON.parse(savedJson) : {}
const request = ref(new AskQuestion(saved))
const qs = queryString(location.search)
if (qs.title) request.value.title = qs.title
if (qs.body) request.value.body = qs.body
if (qs.tags) request.value.tags = qs.tags.split(',')
if (qs.refId || qs.refid) request.value.refId = qs.refId || qs.refid
const previewHtml = ref('')
let allTags = localStorage.getItem('data:tags.txt')?.split('\n') || []
const expandSimilar = ref(true)
const similarQuestions = ref([])

const { createDebounce } = useUtils()
let lastBody = ''
Expand All @@ -47,16 +69,39 @@ export default {
watchEffect(async () => {
debounceApi(request.value.body)
})

watchEffect(async () => {
if ((request.value.title ?? '').trim().length > 19) {
findSimilarQuestions(request.value.title)
}
})

let lastJson = ''
watchEffect(async () => {
const json = JSON.stringify(request.value)
if (json === lastJson) return
localStorage.setItem('ask', json)
lastJson = json
})

async function findSimilarQuestions(text) {
const api = await client.api(new FindSimilarQuestions({ text }))
if (api.succeeded) {
similarQuestions.value = api.response.results || []
}
}

function onSuccess(r) {
localStorage.removeItem('ask')
if (r.redirectTo) {
location.href = r.redirectTo
}
}

onMounted(async () => {
if (allTags.length === 0) {
const txt = await (await fetch('/data/tags.txt')).text()
let txt = await (await fetch('/data/tags.txt')).text()
txt = txt.replace(/\r\n/g,'\n')
localStorage.setItem('data:tags.txt', txt)
allTags = txt.split('\n')
}
Expand All @@ -69,6 +114,6 @@ export default {
}
}

return { request, previewHtml, autoform, configureField, onSuccess }
return { request, previewHtml, autoform, expandSimilar, similarQuestions, configureField, onSuccess }
}
}

0 comments on commit c4605db

Please sign in to comment.