查看一些文檔金格插件WebOffice2015、chrome瀏覽器插件、only-office、UEditor、TinyMCE、CKEditor、wangeditor、canvas-editor
最后選擇了only-office和canvas-editor
only-office非常功能強大,word、ppt、excel都支持在線編輯預覽,還支持協同,又有免費開源版。
附上本地運行demo:
docker run -i -t -d -p 8701:80 onlyoffice/documentserver:版本號
如果是第 1 次執行這個命令,會先去下載 Onlyoffice,比較慢,約等待 3~10 分鐘,網絡暢通一點的會快一些。如果是已經安裝過則直接進行啟動。
先執行 docker ps 查看 Onlyoffice 容器 ID:
# 注意這里要將 id 替換成自己的
docker exec -it f2a3eb675ad1 /bin/bash
然后執行 docker exec -it ID /bin/bash 進入容器,這里將獲取到的 ID 替換一下:
# 啟動所有的內置服務
supervisorctl restart all
# 退出容器
exit
最后訪問 http://IP:8701/example 頁面(這里要注意,IP 不能是 localhost 和 127.0.0.1,一定要用真實 IP 來訪問)
因為開發周期,后端又比較懶不想花時間去看文檔。這一方案被我放棄了
最后選擇了canvas-editor
為什么選它了,開發周期短,界面與word編輯器比較像,可以快速集成到vue,雖然作者沒有開箱即用版。
在vue中主要實現方式就是采用開源項目代碼。
在組件模塊,新建vue文件,html采用開源項目代碼,分3個部分,工具欄,側邊菜單,主要內容,底部工具,旁邊批注。通過import引入開源樣式,注意樣式沖突。在onMounted,采用開源main.ts window.onload代碼。
<div class="menu" editor-component="menu">
...
</div>
<div class="catalog" editor-component="catalog">
...
</div>
<div id="canvasEditor" class="canvas-editor"></div>
<div class="footer-canvas" editor-component="footer">
...
</div>
const instance=new Editor(
document.querySelector('#canvasEditor'),
{
header: props.header,
main: props.main,
footer: props.footer
},
options
);
console.log('實例', instance);
editorRef.value=instance;
// 工具欄方法 例:
// 2. | 撤銷 | 重做 | 格式刷 | 清除格式 |
const undoDom=document.querySelector('.menu-item__undo');
undoDom.title=`撤銷(${isApple ? '?' : 'Ctrl'}+Z)`;
undoDom.onclick=function () {
console.log('undo');
instance.command.executeUndo();
};
<style lang="scss" scoped>
#canvasEditor {
display: flex;
justify-content: center;
background: #f2f4f7;
}
@import url(@/assets/css/dialog.css);
@import url(@/assets/css/signature.css);
</style>
思路通過富文本編輯器實現在線編輯功能,通過插件提供的api獲取圖片base64和文本數據,通過接口保存至數據庫,通過列表數據復顯編輯,后端獲取到圖片數據轉pdf或者其他格式都可以。自己轉pdf則是通過html2canvas jspdf
例:
<canvas-editor
v-if="!loadingInit"
:header="canvasEditor.header"
:main="canvasEditor.main"
:footer="canvasEditor.footer"
:options="canvasEditor.options"
@getCanvasEditorData="getCanvasEditorData"
/>
import CanvasEditor from '@/components/editor/index.vue';
components: {
CanvasEditor
},
canvasEditor: {
main: [],
options: [],
header: [],
footer: []
},
// getCanvasEditorData 事件
主要獲取富文本 main內容 image的base64 options配置項 用于數據保存
getCanvasEditorData: ({ value, imgage, options })=> {
},
// 頁眉配置
header: {
type: Array,
default: []
},
// 主要編輯內容
main: {
type: Array,
default: []
},
// 頁腳信息
footer: {
type: Array,
default: []
},
//
options: {
type: Object,
default: {}
},
// 批注 TODO
commentList: {
type: Array,
default: []
}
interface IEditorOption {
mode?: EditorMode // 編輯器模式:編輯、清潔(不顯示視覺輔助元素。如:分頁符)、只讀、表單(僅控件內可編輯)、打印(不顯示輔助元素、未書寫控件及前后括號)。默認:編輯
defaultType?: string // 默認元素類型。默認:TEXT
defaultColor?: string // 默認字體顏色。默認:#000000
defaultFont?: string // 默認字體。默認:Microsoft YaHei
defaultSize?: number // 默認字號。默認:16
minSize?: number // 最小字號。默認:5
maxSize?: number // 最大字號。默認:72
defaultBasicRowMarginHeight?: number // 默認行高。默認:8
defaultRowMargin?: number // 默認行間距。默認:1
defaultTabWidth?: number // 默認tab寬度。默認:32
width?: number // 紙張寬度。默認:794
height?: number // 紙張高度。默認:1123
scale?: number // 縮放比例。默認:1
pageGap?: number // 紙張間隔。默認:20
underlineColor?: string // 下劃線顏色。默認:#000000
strikeoutColor?: string // 刪除線顏色。默認:#FF0000
rangeColor?: string // 選區顏色。默認:#AECBFA
rangeAlpha?: number // 選區透明度。默認:0.6
rangeMinWidth?: number // 選區最小寬度。默認:5
searchMatchColor?: string // 搜索高亮顏色。默認:#FFFF00
searchNavigateMatchColor?: string // 搜索導航高亮顏色。默認:#AAD280
searchMatchAlpha?: number // 搜索高亮透明度。默認:0.6
highlightAlpha?: number // 高亮元素透明度。默認:0.6
resizerColor?: string // 圖片尺寸器顏色。默認:#4182D9
resizerSize?: number // 圖片尺寸器大小。默認:5
marginIndicatorSize?: number // 頁邊距指示器長度。默認:35
marginIndicatorColor?: string // 頁邊距指示器顏色。默認:#BABABA
margins?: IMargin // 頁面邊距。默認:[100, 120, 100, 120]
pageMode?: PageMode // 紙張模式:連頁、分頁。默認:分頁
tdPadding?: IPadding // 單元格內邊距。默認:[0, 5, 5, 5]
defaultTrMinHeight?: number // 默認表格行最小高度。默認:42
defaultColMinWidth?: number // 默認表格列最小寬度(整體寬度足夠時應用,否則會按比例縮小)。默認:40
defaultHyperlinkColor?: string // 默認超鏈接顏色。默認:#0000FF
header?: IHeader // 頁眉信息。{top?:number; maxHeightRadio?:MaxHeightRatio;}
footer?: IFooter // 頁腳信息。{bottom?:number; maxHeightRadio?:MaxHeightRatio;}
pageNumber?: IPageNumber // 頁碼信息。{bottom:number; size:number; font:string; color:string; rowFlex:RowFlex; format:string; numberType:NumberType;}
paperDirection?: PaperDirection // 紙張方向:縱向、橫向
inactiveAlpha?: number // 正文內容失焦時透明度。默認值:0.6
historyMaxRecordCount?: number // 歷史(撤銷重做)最大記錄次數。默認:100次
printPixelRatio?: number // 打印像素比率(值越大越清晰,但尺寸越大)。默認:3
maskMargin?: IMargin // 編輯器上的遮蓋邊距(如懸浮到編輯器上的菜單欄、底部工具欄)。默認:[0, 0, 0, 0]
letterClass?: string[] // 排版支持的字母類。默認:a-zA-Z。內置可選擇的字母表類:LETTER_CLASS
contextMenuDisableKeys?: string[] // 禁用的右鍵菜單。默認:[]
scrollContainerSelector?: string // 滾動區域選擇器。默認:document
wordBreak?: WordBreak // 單詞與標點斷行:BREAK_WORD首行不出現標點&單詞不拆分、BREAK_ALL按字符寬度撐滿后折行。默認:BREAK_WORD
watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string;}
control?: IControlOption // 控件信息。 {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string;}
checkbox?: ICheckboxOption // 復選框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;}
radio?: IRadioOption // 單選框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;}
cursor?: ICursorOption // 光標樣式。{width?: number; color?: string; dragWidth?: number; dragColor?: string;}
title?: ITitleOption // 標題配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // 編輯器空白占位文本
group?: IGroup // 成組配置。{opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean}
pageBreak?: IPageBreak // 分頁符配置。{font?:string; fontSize?:number; lineDash?:number[];}
zone?: IZoneOption // 編輯器區域配置。{tipDisabled?:boolean;}
background?: IBackgroundOption // 背景配置。{color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat;}。默認:{color: '#FFFFFF'}
lineBreak?: ILineBreakOption // 換行符配置。{disabled?:boolean; color?:string; lineWidth?:number;}
separator?: ISeparatorOption // 分隔符配置。{lineWidth?:number; strokeStyle?:string;}
}
interface IHeader {
top?: number // 距離頁面頂部大小。默認:30
maxHeightRadio?: MaxHeightRatio // 占頁面最大高度比。默認:HALF
disabled?: boolean // 是否禁用
}
interface IPageNumber {
bottom?: number // 距離頁面底部大小。默認:60
size?: number // 字體大小。默認:12
font?: string // 字體。默認:Microsoft YaHei
color?: string // 字體顏色。默認:#000000
rowFlex?: RowFlex // 行對齊方式。默認:CENTER
format?: string // 頁碼格式。默認:{pageNo}。示例:第{pageNo}頁/共{pageCount}頁
numberType?: NumberType // 數字類型。默認:ARABIC
disabled?: boolean // 是否禁用
startPageNo?: number // 起始頁碼。默認:1
fromPageNo?: number // 從第幾頁開始出現頁碼。默認:0
}
interface IWatermark {
data: string // 文本。
color?: string // 顏色。默認:#AEB5C0
opacity?: number // 透明度。默認:0.3
size?: number // 字體大小。默認:200
font?: string // 字體。默認:Microsoft YaHei
}
@{
ViewData["Title"]="Index";
}
<h1>Index</h1>
<!--上傳視頻-->
<div>
<div>
<div>
<div>
<span class="">上傳視頻</span>
</div>
<input id="uploadvideofile" type="file" accept="video/mp4,video/quicktime" class="up-video" />
</div>
</div>
<div id="videoplayer" style="display: none"></div>
<div id="videooutput"></div>
<div>
<p style="color: #797979; margin-top: 5px; font-size: 12px;">視頻格式必須為: mp4或mov。視頻時長須在15秒以內,超出時長系統將自動截取前15秒內容。</p>
</div>
</div>
<script src="~/Scripts/ajax.1.5.2.js"></script>
<!--傳視頻相關-->
<script type="text/javascript">
$('#uploadvideofile').change(function() {
var file, videoURL, windowURL;
var filemaxsize=1024 * 1024 * 20; //200M
if (fileValid(this, filemaxsize, 'video')) {
file=this.files[0];
try {
var temp=ajax.upload_big(
"UpLoadBigFile/UploadVideo?opration=2222", //文件上傳地址
"#uploadvideofile", //input=file 選擇器
1024 * 1024, //切割文件大小
"*", //文件限制類型 mime類型
function(index) { //上傳成功事件
//console.log("slice [ " + index + "] uploaded")
},
function(index, length) { //上傳進度事件
//console.log(index + "/" + length);
//var ratio=(index / length) * 100.00 | 0.00;
//var progress=$(".progress > div");
//progress.css("width", ratio + "%");
//progress.attr("aria-valuenow", index);
//progress.attr("aria-valuemax", length);
//progress.children("span").text("" + ratio + "%");
},
function(index, length) { //超時處理事件
console.log(index + "/" + length);
}
);
} catch (e) {
console.log(e.name + " " + e.message);
}
videoURL=null;
windowURL=window.URL || window.webkitURL;
videoURL=windowURL.createObjectURL(file);
$('#videoplayer').html('<video src="' + videoURL + '" controls="controls"></video>');
setTimeout(function() { createIMG(); }, 800);
}
});
//驗證上傳文件大小和類型
/**
*
* param {this} value_ [獲取input對象,一般為this]
* param {[number]} size_ [文件限制的大小,單位為kb]
* param {[string]} type_ [文件類別]
* param {[function]} callback [驗證通過的回調]
*/
function fileValid(value_, size_, type_, callback) {
var file=value_.files[0];
var fileSize=(file.size / 1024).toFixed(0); //文件大小
var fileType=value_.value.substring(value_.value.lastIndexOf(".")); //文件類型
if (fileSize > size_) {
alert('視頻過大,請選擇小于200MB的視頻!');
return false;
}
switch (type_) {
case 'video':
if (!fileType.match(/.mp4|.mov/i)) {
alert('請上傳正確格式的視頻!');
return false;
}
break;
default:
alert('參數設置不正確!');
return false;
break;
}
return true;
}
var createIMG=function() {
var scale=0.25,
video=$('#videoplayer').find('video')[0],
canvas=document.createElement("canvas"),
canvasFill=canvas.getContext('2d');
canvas.width=video.videoWidth * scale;
canvas.height=video.videoHeight * scale;
canvasFill.drawImage(video, 0, 0, canvas.width, canvas.height);
var src=canvas.toDataURL("image/jpeg");
$('#videooutput').html('<img id="imgSmallView" src="' + src + '" alt="預覽圖" />');
}
</script>
<script>
(function () {
//$("#uploadvideofile").change(function () {
// var temp=ajax.upload_big(
// "AjaxHandlers/UploadVideoHandler.ashx", //文件上傳地址
// "#uploadvideofile", //input=file 選擇器
// 1024 * 1024, //切割文件大小
// "*", //文件限制類型 mime類型
// function(index) { //上傳成功事件
// //console.log("slice [ " + index + "] uploaded")
// },
// function(index, length) { //上傳進度事件
// //console.log(index + "/" + length);
// //var ratio=(index / length) * 100.00 | 0.00;
// //var progress=$(".progress > div");
// //progress.css("width", ratio + "%");
// //progress.attr("aria-valuenow", index);
// //progress.attr("aria-valuemax", length);
// //progress.children("span").text("" + ratio + "%");
// },
// function(index, length) { //超時處理事件
// console.log(index + "/" + length);
// }
// );
// console.log(temp);
//});
})();
</script>
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using VideoDemo.Common;
using VideoDemo.Models;
namespace VideoDemo.Controllers
{
public class UploadBigFileController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
//[ValidateAntiForgeryToken]
public int UploadVideo([FromForm] VideoFileModel filemodel, [FromQuery]string opration)
{
try
{
int slicenum=FileHelper.UploadVideo(filemodel);
return slicenum;
//return RedirectToAction(nameof(Index));
}
catch(Exception e)
{
Console.Write(e.ToString());
return -1;
}
}
public ActionResult Details(int id)
{
return View();
}
public ActionResult Create()
{
return View();
}
// GET: UpLoadBigFileController/Edit/5
public ActionResult Edit(int id)
{
return View();
}
// POST: UpLoadBigFileController/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, IFormCollection collection)
{
try
{
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
}
}
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using VideoDemo.Models;
using Xabe.FFmpeg;
namespace VideoDemo.Common
{
public class FileHelper
{
private static string finalpath="";
public static int UploadVideo(VideoFileModel videoFile)
{
//前端傳輸是否為切割文件最后一個小文件
var isLast=videoFile.isLast;
//前端傳輸當前為第幾次切割小文件
var count=videoFile.Count;
//獲取前端處理過的傳輸文件名
string fileName=videoFile.Name;
//存儲接受到的切割文件
if (videoFile.Files.Length <=0)
{
return -1;
}
IFormFile formFile=videoFile.Files;
//處理文件名稱(去除.part*,還原真實文件名稱)
string newFileName=fileName.Substring(0, fileName.LastIndexOf('.'));
//臨時存儲文件夾路徑
string desPath=Directory.GetCurrentDirectory() + @"/uploads/slice/" + newFileName;
//判斷指定目錄是否存在臨時存儲文件夾,沒有就創建
if (!System.IO.Directory.Exists(desPath))
{
//不存在就創建目錄
System.IO.Directory.CreateDirectory(desPath);
}
//存儲文件
using (var stream=new FileStream(desPath+"/"+ fileName, FileMode.Create))
{
formFile.CopyTo(stream);
}
//file.SaveAs("E:\\uploads\\slice\\" + newFileName + "\\" + fileName);
//判斷是否為最后一次切割文件傳輸
if (isLast=="true")
{
//判斷組合的文件是否存在
finalpath=Directory.GetCurrentDirectory() + @"/uploads/" + newFileName;
if (File.Exists(finalpath)) //如果文件存在
{
File.Delete(finalpath); //先刪除,否則新文件就不能創建
}
//創建空的文件流
FileStream FileOut=new FileStream(finalpath, FileMode.CreateNew, FileAccess.ReadWrite);
BinaryWriter bw=new BinaryWriter(FileOut);
//獲取臨時存儲目錄下的所有切割文件
string[] allFile=Directory.GetFiles(desPath);
//將文件進行排序拼接
allFile=allFile.OrderBy(s=> int.Parse(Regex.Match(s, @"\d+$").Value)).ToArray();
for (int i=0; i < allFile.Length; i++)
{
FileStream FileIn=new FileStream(allFile[i], FileMode.Open);
BinaryReader br=new BinaryReader(FileIn);
byte[] data=new byte[1048576]; //流讀取,緩存空間
int readLen=0; //每次實際讀取的字節大小
readLen=br.Read(data, 0, data.Length);
bw.Write(data, 0, readLen);
//關閉輸入流
FileIn.Close();
}
//關閉二進制寫入
bw.Close();
FileOut.Close();
ClipVideo();
}
return int.Parse(count) + 1;
}
public static async void ClipVideo()
{
/**
* 支持視頻格式:mpeg,mpg,avi,dat,mkv,rmvb,rm,mov.
*不支持:wmv
* **/
//FFmpeg.SetExecutablesPath("ffmpeg.exe");
//ffmpeg.exe的路徑,控制臺程序會在執行目錄(....FFmpeg測試\bin\Debug)下找此文件,
//視頻路徑
string finalname=Path.GetFileNameWithoutExtension(finalpath);
string videoFilePath=finalpath; //"d:\\01.avi";
finalpath=finalpath.Replace(finalname, finalname + "15S");
//IMediaInfo videoFile=await FFmpeg.GetMediaInfo(videoFilePath);
//TimeSpan totaotp=videoFile.Duration;
//string totalTime=string.Format("{0:00}:{1:00}:{2:00}", (int)totaotp.TotalHours, totaotp.Minutes,totaotp.Seconds);
#region 自定義邏輯
//string sourceFileName=Path.GetFileName(upFile.get_FileName()); //取出上傳的視頻的文件名,進而取出該文件的擴展名
//string sourceFileName="02.avi";
//string flv_file=System.IO.Path.ChangeExtension("d:\\01.avi", ".flv");
//string Command=" -i \"" + FromName + "\" -y -ab 32 -ar 22050 -b 800000 -s 480*360 \"" + ExportName + "\""; //Flv格式
//轉換視頻為flv
//ffmpeg -i F:\01.wmv -ab 56 -ar 22050 -b 500 -r 15 -s 320x240 f:\test.flv
//視頻截圖,fileName視頻地址,imgFile圖片地址
//ffmpeg -i input.flv -y -f image2 -ss 10.11 -t 0.001 -s 240x180 catchimg.jpg;
//ImgstartInfo.Arguments=" -i " + fileName + " -y -f image2 -ss 2 -vframes 1 -s " + FlvImgSize + " " + flv_img;
//string Command=" -i \"test.wmv\" -y -ab 32 -ar 22050 -b 800000 -s 320*240 \"2.flv\"";
//string Command="E:\\FFmpeg\\ffmpeg.exe -i E:\\ClibDemo\\VideoPath\\admin\\a.wmv -y -ab 56 -ar 22050 -b 500 -r 15 -s 320*240 " ExportName;
//3.重新編碼進行剪切
//ffmpeg -ss [start] -t [duration] -i [in].mp4 -c:v libx264 -c:a aac -strict experimental -b:a 98k [out].mp4
//相對來說比較精確,可是還是不是特別精確
//string Command=" -ss 00:00:00 -t 00:00:15 -i d:\\01.avi d:\\output.avi"; // ffmpeg -ss 00:00:10 -t 00:01:22 -i 五月天-突然好想你.mp3 out.mp3
string Command=" -ss 00:00:00 -t 00:00:15 -i " + videoFilePath + " " + finalpath;
string Command1= " -i d:\\01.avi -vf \"drawtext=fontfile=simhei.ttf: text='南通極客如皋張HC':x=w-tw-10:y=10:fontsize=28:fontcolor=red:shadowy=2\" d:\\output1.avi";
string Command2= " -i d:\\01.avi -vf \"drawtext=fontfile=simhei.ttf: text='南通極客QQ(827XXXXXX)':y=h-line_h-10:x=(w-mod(30*n\\,w+tw)):fontsize=34:fontcolor=yellow:shadowy=2\" d:\\output2.avi";
System.Diagnostics.Process p=new System.Diagnostics.Process();
//非控制臺程序必須寫完整路徑
p.StartInfo.FileName=AppContext.BaseDirectory + ("/ffmpeg.exe");
p.StartInfo.Arguments=Command;
//p.StartInfo.Arguments=Command1;
//Asp.net 獲取當前目錄
p.StartInfo.WorkingDirectory=AppContext.BaseDirectory; //HttpContext.Current.Request.MapPath("~/"); //Environment.CurrentDirectory;
p.StartInfo.UseShellExecute=false;
p.StartInfo.RedirectStandardInput=true;
p.StartInfo.RedirectStandardOutput=true;
p.StartInfo.RedirectStandardError=true;
p.StartInfo.CreateNoWindow=false;
//開始執行
p.Start();
p.BeginErrorReadLine();
p.WaitForExit();
p.Close();
p.Dispose();
#endregion
//Console.WriteLine("時間長度:{0}", totalTime);
//Console.WriteLine("高度:{0}", videoFile.Height);
//Console.WriteLine("寬度:{0}", videoFile.Width);
//Console.WriteLine("數據速率:{0}", videoFile.VideoBitRate);
//Console.WriteLine("數據格式:{0}", videoFile.VideoFormat);
//Console.WriteLine("比特率:{0}", videoFile.BitRate);
//Console.WriteLine("文件路徑:{0}", videoFile.Path);
Console.ReadKey();
}
}
}
ainless Lab 是 Elasticsearch 7.13 引入的實驗性功能,是一個交互式代碼編輯器,可以實時測試和調試 Painless 腳本。
本文展開解讀 Painless Lab 如何應用于企業級實戰開發中的腳本調試環節!
Painless Lab是一個交互式的測試版代碼編輯器,用于實時測試和調試Painless腳本。
咱們可以通過打開主菜單,點擊開發工具,然后選擇 Painless Lab 來訪問它。
如下圖所示,左側是:腳本輸入區域。右側由三部分組成:
一句話:Painless Lab 可以實時測試和調試 Painless 腳本。
Painless Lab 允許我們創建 Kibana 運行時字段(runtime fields)、處理重新索引的數據(reindex)、定義復雜的 Watcher 條件(付費功能),并在其他上下文中處理數據。
下面的 Context 部分展開就是 Painless Lab 的核心功能區域。
三種類型進一步展開:
進一步再展開解讀。
https://www.elastic.co/guide/en/elasticsearch/painless/8.11/painless-execute-api.html#_contexts
上下文描述painless_test默認上下文,如果沒有指定其他上下文則使用此上下文。用于通用腳本測試,例如調試和驗證腳本邏輯。filter將腳本視為在腳本查詢中運行。用于過濾數據。score將腳本視為在 function_score 查詢中的 script_score 函數中運行。用于評分數據。
默認上下文,如果沒有指定其他上下文則使用此上下文。用于通用腳本測試,例如調試和驗證腳本邏輯。
將腳本視為在腳本查詢中運行。用于過濾數據。
將腳本視為在 function_score 查詢中的 script_score 函數中運行。用于評分數據。
我們逐一詳盡展開解讀,確保大家跟著過一遍,就能學得會!
Basic 上下文允許我們獨立測試腳本邏輯,并將結果轉換為字符串輸出。
樣例數據可以放到 params 中作為輸入。
實戰舉例如下:
對于 Ingest pipeline 腳本,參考官方示例,由于腳本是在 Ingest Pipeline 中處理數據的,并且沒有涉及到查詢過濾 filter 或評分 score,因此 Basic 上下文是最合適的選擇。
https://www.elastic.co/guide/en/elasticsearch/reference/current/script-processor.html
如上圖所示,我們在 parameters 中輸入如下數據:
{
"ctx": {
"env": "小米-筆記本-電腦-雷軍"
},
"params": {
"delimiter": "-",
"position": 1
}
}
我們在左側輸入如下的 painless script 腳本,如下所示:
// 獲取 ctx 數據
def ctx=params.ctx;
// 獲取參數
def delimiter=params.params.delimiter;
def position=params.params.position;
// 執行腳本邏輯
String[] envSplit=ctx.env.splitOnToken(delimiter);
ArrayList tags=new ArrayList();
//tags.add(envSplit[position].trim());
for (def tag : envSplit) {
tags.add(tag.trim());
}
ctx.tags=tags;
// 返回結果以供調試
return ctx.tags;
執行結果如下圖右側 Output 所示。
上述腳本實現的核心功能就是:以分隔符截斷字符串,形成獨立字符串,插入到 tags 集合中。
這樣調試過之后,再微調一下就可以應用到 ingest pipeline 中。
區別于剛才的邏輯,這里需要我們先創建索引,然后基于我們構造的索引數據進行展開 filter 過濾檢索。
POST /hockey/_doc/1
{
"first": "johnny",
"last": "gaudreau",
"goals": [9, 27, 1],
"assists": [17, 46, 0],
"gp": [26, 82, 1]
}
POST /hockey/_doc/2
{
"first": "john",
"last": "doe",
"goals": [2, 3, 4],
"assists": [1, 2, 3],
"gp": [4, 5, 6]
}
上述索引必須構建,否則會報錯如下圖所示。
錯誤原因可能是:索引不存在或者Mapping 不存在。
正確的執行步驟如下所示:
結合上面三個步驟以及左側的腳本,主要驗證左側腳本正確與否。注意:返回值必須是 Bool 類型。執行結果如下:
最終結合上述調試成功的腳本,整合到 script query 檢索語句中,就能得到滿足用戶預期的結果數據。
POST /hockey/_search
{
"query": {
"bool": {
"filter": {
"script": {
"script": {
"lang": "painless",
"source": """
// 獲取 goals 字段的值
def goals=doc['goals'];
// 初始化總和變量
int sum=0;
// 遍歷 goals 數組并計算總和
for (int i=0; i < goals.size(); i++) {
sum +=goals.get(i);
}
// 輸出調試信息
//Debug.explain(sum);
// 返回 true 以匹配所有文檔,僅用于調試目的
return sum>10;
"""
}
}
}
}
}
}
在 Elasticsearch 中,score 類型調試上下文用于在 function_score 查詢中的 script_score 函數中運行腳本。
該方式允許用戶編寫腳本來動態計算文檔的評分,從而影響搜索結果的排序。
假設我們有一個包含產品信息的索引 products,每個文檔包含以下字段:
我們希望根據價格和評分來動態計算每個產品的分數,具體規則如下:
POST /products/_doc/1
{
"name": "Product A",
"price": 100,
"rating": 4.5
}
POST /products/_doc/2
{
"name": "Product B",
"price": 200,
"rating": 4.0
}
POST /products/_doc/3
{
"name": "Product C",
"price": 150,
"rating": 3.5
}
我們將編寫一個 function_score 查詢,使用 Painless 腳本來計算每個文檔的分數,并根據計算結果排序。
核心邏輯:
在 Painless Lab 中,可以使用類似的腳本來調試和驗證評分邏輯:
{
"price": 100,
"rating": 4.5
}
左側腳本部分
// 獲取參數值
long price=params.price;
double rating=params.rating;
// 檢查字段值是否存在
if (price==0 || rating==0) {
// 如果任一字段值為 0,則返回默認分數(例如 0)
return 0.0;
}
// 自定義評分邏輯
double score=(1 / (float)price) * rating;
// 返回評分結果
return score;
執行結果如下所示:
上述腳本通過使用 score 上下文中的 script_score 函數,可以根據自定義邏輯動態計算文檔的分數,從而影響搜索結果的排序。
這在需要根據復雜規則排序搜索結果時非常有用。
通過在 Painless Lab 中調試和驗證上述腳本,可以確保評分邏輯的正確性和有效性。
進而,可以組合寫出如下的評分腳本檢索語句。
POST /products/_search
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"script_score": {
"script": {
"source": """
long price=doc['price'].value;
double rating=doc['rating'].value;
// 自定義評分邏輯
double score=(1 / (float)price) * rating;
return score;
"""
}
},
"boost_mode": "replace" // 使用腳本計算的分數替換原始分數
}
}
}
實現核心:根據自定義邏輯計算分數:score=(1 / price) * rating。價格越低,評分越高,分數越高。
boost_mode: 設置為 replace,使用腳本計算的分數替換原始分數。
Kibana Painless Lab 是 Elasticsearch 7.13 引入的實驗性功能,為開發者提供交互式代碼編輯器,用于實時測試和調試 Painless 腳本。
通過 painless_test、filter 和 score 上下文三種測試方式,開發者可以創建和調試 Kibana 運行時字段、處理重新索引的數據、定義復雜的 Watcher 條件,并根據復雜規則動態計算文檔分數,提高腳本開發和優化效率。
作者:銘毅天下
來源-微信公眾號:銘毅天下Elasticsearch
出處:https://mp.weixin.qq.com/s/x9FrVCyrmpvJsc7pNGBm2g
*請認真填寫需求信息,我們會在24小時內與您取得聯系。